├── .gitignore ├── LICENSE ├── README.md ├── circlet.c ├── circlet_lib.janet ├── mongoose.c ├── mongoose.h ├── project.janet └── test └── testserv.janet /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Calvin Rose 2 | Copyright (c) 2004-2013 Sergey Lyubka 3 | Copyright (c) 2013-2018 Cesanta Software Limited 4 | All rights reserved 5 | 6 | This software is dual-licensed: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License version 2 as 8 | published by the Free Software Foundation. For the terms of this 9 | license, see . 10 | 11 | You are free to use this software under the terms of the GNU General 12 | Public License, but WITHOUT ANY WARRANTY; without even the implied 13 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | See the GNU General Public License for more details. 15 | 16 | Alternatively, you can license this software under a commercial 17 | license, as set out in . 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circlet 2 | 3 | Circlet is an HTTP and networking library for the 4 | [janet](https://github.com/janet-lang/janet) language. 5 | It provides an abstraction out of the box like Clojure's 6 | [ring](https://github.com/ring-clojure/ring), which is a server abstraction 7 | that makes it easy to build composable web applications. 8 | 9 | Circlet uses [mongoose](https://cesanta.com/) as the underlying HTTP server 10 | engine. Mongoose is a portable, low footprint, event based server library. The 11 | flexible build system requirements of mongoose make it very easy to embed 12 | in other C programs and libraries. 13 | 14 | ## Installing 15 | 16 | You can add Circlet as a dependency in your `project.janet`: 17 | 18 | ```clojure 19 | (declare-project 20 | :name "web framework" :description "A framework for web development" 21 | :dependencies ["https://github.com/janet-lang/circlet.git"]) 22 | ``` 23 | 24 | You can also install it system-wide with `jpm`: 25 | 26 | ``` 27 | sh jpm install circlet 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Creating a server 33 | 34 | You can create a HTTP server using the `circlet/server` function. The 35 | function is of the form `(circlet/server handler port &opt ip-address)` 36 | and takes the following parameters: 37 | 38 | - `handler` function that takes the incoming HTTP request object (explained in 39 | greater detail below) and returns the HTTP response object. 40 | - `port` number of the port on which the server will listen for incoming 41 | requests. 42 | - `ip-address` optional string representing the IP address on which the server 43 | will listen (defaults to `“127.0.0.1”`). The address `“*”` will 44 | cause the server to listen on all available IP addresses. 45 | 46 | The server runs immediately after creation. 47 | 48 | ### Request 49 | 50 | The `handler` function takes a single parameter representing the request. The 51 | request is a Janet table containing all the information about the request. It 52 | contains the following keys: 53 | 54 | - `:uri` requested URI 55 | - `:method` HTTP method of the request as a Janet string (e.g. "GET", "POST") 56 | - `:protocol` version of the HTTP protocol used for request 57 | - `:headers` HTTP headers sent with the request as a Janet table. Keys in this 58 | table are Janet strings with standard header's name (e.g. "Host", “Accept"). 59 | Values are the values in the HTTP header. 60 | - `:body` body of the HTTP request 61 | - `:query-string` query string part of the requested URI 62 | - `:connection` internal mongoose connection serving this request 63 | 64 | ### Response 65 | 66 | The return value of the `handler` function must be a Janet table containing 67 | at least the `status` key with an integer value that corresponds to the HTTP 68 | status of the response (e.g. 200 for success). 69 | 70 | Other possible keys include: 71 | 72 | - `:body` the body of the HTTP response (e.g. a string in HTML or JSON) 73 | - `:headers` a Janet table or struct with standard HTTP headers. The structure 74 | is the same as the HTTP request case described above. 75 | 76 | There is also special key `:kind` you can use. There are two possible values for 77 | this key: 78 | 79 | - `:file` for serving a file from the filesystem. The filename is specified by 80 | the `:file` key. You can specify `:mime` key with value of corresponding 81 | mime type, it defaults to text/html. 82 | - `:static` for serving static file from the filesystem. You have to provide 83 | `:root` key with value of the path you want to serve. 84 | 85 | ### Middleware 86 | 87 | Circlet also allows for the creation of different “middleware”. Pieces 88 | of middleware can be thought of as links in a chain of functions that are 89 | used to consume the HTTP request. The `handler` function can be thought of 90 | as a piece of middleware and other middleware should match the signature and 91 | return type of the `handler` function, i.e. accept and return a Janet table. 92 | 93 | Middleware can be created in one of two ways. A user can define a function 94 | with the appropriate signature and return type or use Circlet’s 95 | `circlet/middleware` function to coerce an argument into a piece of 96 | middleware. Middleware pieces are often higher-order functions (meaning 97 | that they return another function). This allows for parameterization at 98 | creation time. 99 | 100 | #### Provided middleware 101 | 102 | There are three basic pieces of middleware provided by Circlet: 103 | 104 | - `(circlet/router routes)` simple routing facility. This function takes a 105 | Janet table containing the routes. Each key should be a Janet string matching 106 | a URI (e.g. `”/“`, `”/posts"`) with a value that is a function of the 107 | same form as the `handler` function described above. 108 | - `(circlet/logger nextmw)` simple logging facility. This function prints 109 | the request info on `stdout`. The only argument is the next middleware. 110 | - `(circlet/cookies nextmw)` middleware which extracts the cookies from the 111 | HTTP header and stores the value under the `:cookies` key in the request 112 | object. 113 | 114 | ## Example 115 | 116 | The below example starts a very simple web server on port 8000. 117 | 118 | ```clojure 119 | (import circlet) 120 | 121 | (defn myserver 122 | "A simple HTTP server" [request] 123 | {:status 200 124 | :headers {"Content-Type" "text/html"} :body "

Hello.

"}) 125 | 126 | (circlet/server myserver 8000) 127 | ``` 128 | 129 | ## Development 130 | 131 | ### Building 132 | 133 | Building requires [Janet](https://github.com/janet-lang/janet) to be installed 134 | on the system, as well as the `jpm` tool (installed by default with Janet). 135 | 136 | ```sh 137 | jpm build 138 | ``` 139 | 140 | You can also just run `jpm` to see a list of possible build commands. 141 | 142 | ### Testing 143 | 144 | Run a server on localhost with the following command 145 | 146 | ```sh 147 | jpm test 148 | ``` 149 | 150 | This example is more involved, and shows all the functionality described in this 151 | document. 152 | 153 | ## License 154 | 155 | Unlike [janet](https://github.com/janet-lang/janet), Circlet is licensed 156 | under the GPL license in accordance with mongoose. 157 | -------------------------------------------------------------------------------- /circlet.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mongoose.h" 3 | #include 4 | 5 | typedef struct { 6 | struct mg_connection *conn; 7 | JanetFiber *fiber; 8 | } ConnectionWrapper; 9 | 10 | static int connection_mark(void *p, size_t size) { 11 | (void) size; 12 | ConnectionWrapper *cw = (ConnectionWrapper *)p; 13 | struct mg_connection *conn = cw->conn; 14 | JanetFiber *fiber = cw->fiber; 15 | janet_mark(janet_wrap_fiber(fiber)); 16 | janet_mark(janet_wrap_abstract(conn->mgr)); 17 | return 0; 18 | } 19 | 20 | static struct JanetAbstractType Connection_jt = { 21 | "mongoose.connection", 22 | NULL, 23 | connection_mark, 24 | #ifdef JANET_ATEND_GCMARK 25 | JANET_ATEND_GCMARK 26 | #endif 27 | }; 28 | 29 | static int manager_gc(void *p, size_t size) { 30 | (void) size; 31 | mg_mgr_free((struct mg_mgr *) p); 32 | return 0; 33 | } 34 | 35 | static int manager_mark(void *p, size_t size) { 36 | (void) size; 37 | struct mg_mgr *mgr = (struct mg_mgr *)p; 38 | /* Iterate all connections, and mark then */ 39 | struct mg_connection *conn = mgr->active_connections; 40 | while (conn) { 41 | ConnectionWrapper *cw = conn->user_data; 42 | if (cw) { 43 | janet_mark(janet_wrap_abstract(cw)); 44 | } 45 | conn = conn->next; 46 | } 47 | return 0; 48 | } 49 | 50 | static struct JanetAbstractType Manager_jt = { 51 | "mongoose.manager", 52 | manager_gc, 53 | manager_mark, 54 | #ifdef JANET_ATEND_GCMARK 55 | JANET_ATEND_GCMARK 56 | #endif 57 | }; 58 | 59 | static Janet cfun_poll(int32_t argc, Janet *argv) { 60 | janet_fixarity(argc, 2); 61 | struct mg_mgr *mgr = janet_getabstract(argv, 0, &Manager_jt); 62 | int32_t wait = janet_getinteger(argv, 1); 63 | mg_mgr_poll(mgr, wait); 64 | return argv[0]; 65 | } 66 | 67 | static Janet mg2janetstr(struct mg_str str) { 68 | return janet_stringv((const uint8_t *) str.p, str.len); 69 | } 70 | 71 | /* Turn a string value into c string */ 72 | static const char *getstring(Janet x, const char *dflt) { 73 | if (janet_checktype(x, JANET_STRING)) { 74 | const uint8_t *bytes = janet_unwrap_string(x); 75 | return (const char *)bytes; 76 | } else { 77 | return dflt; 78 | } 79 | } 80 | 81 | static Janet build_http_request(struct mg_connection *c, struct http_message *hm) { 82 | JanetTable *payload = janet_table(10); 83 | janet_table_put(payload, janet_ckeywordv("body"), mg2janetstr(hm->body)); 84 | janet_table_put(payload, janet_ckeywordv("uri"), mg2janetstr(hm->uri)); 85 | janet_table_put(payload, janet_ckeywordv("query-string"), mg2janetstr(hm->query_string)); 86 | janet_table_put(payload, janet_ckeywordv("method"), mg2janetstr(hm->method)); 87 | janet_table_put(payload, janet_ckeywordv("protocol"), mg2janetstr(hm->proto)); 88 | janet_table_put(payload, janet_ckeywordv("connection"), janet_wrap_abstract(c->user_data)); 89 | /* Add headers */ 90 | JanetTable *headers = janet_table(5); 91 | for (int i = 0; i < MG_MAX_HTTP_HEADERS; i++) { 92 | if (hm->header_names[i].len == 0) 93 | break; 94 | Janet key = mg2janetstr(hm->header_names[i]); 95 | Janet value = mg2janetstr(hm->header_values[i]); 96 | Janet header = janet_table_get(headers, key); 97 | switch (janet_type(header)) { 98 | case JANET_NIL: 99 | janet_table_put(headers, key, value); 100 | break; 101 | case JANET_ARRAY: 102 | janet_array_push(janet_unwrap_array(header), value); 103 | break; 104 | default: 105 | { 106 | Janet newHeader[2] = { header, value }; 107 | janet_table_put(headers, key, janet_wrap_array(janet_array_n(newHeader, 2))); 108 | break; 109 | } 110 | } 111 | } 112 | janet_table_put(payload, janet_ckeywordv("headers"), janet_wrap_table(headers)); 113 | return janet_wrap_table(payload); 114 | } 115 | 116 | /* Send an HTTP reply. This should try not to panic, as at this point we 117 | * are outside of the janet interpreter. Instead, send a 500 response with 118 | * some formatted error message. */ 119 | static void send_http(struct mg_connection *c, Janet res, void *ev_data) { 120 | switch (janet_type(res)) { 121 | default: 122 | mg_send_head(c, 500, 0, ""); 123 | break; 124 | case JANET_TABLE: 125 | case JANET_STRUCT: 126 | { 127 | const JanetKV *kvs; 128 | int32_t kvlen, kvcap; 129 | janet_dictionary_view(res, &kvs, &kvlen, &kvcap); 130 | 131 | /* Get response kind and check for special handling methods. */ 132 | Janet kind = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("kind")); 133 | if (janet_checktype(kind, JANET_KEYWORD)) { 134 | const uint8_t *kindstr = janet_unwrap_keyword(kind); 135 | 136 | /* Check for serving static files */ 137 | if (!janet_cstrcmp(kindstr, "static")) { 138 | /* Construct static serve options */ 139 | struct mg_serve_http_opts opts; 140 | memset(&opts, 0, sizeof(opts)); 141 | Janet root = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("root")); 142 | opts.document_root = getstring(root, NULL); 143 | mg_serve_http(c, (struct http_message *) ev_data, opts); 144 | return; 145 | } 146 | 147 | /* Check for serving single file */ 148 | if (!janet_cstrcmp(kindstr, "file")) { 149 | Janet filev = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("file")); 150 | Janet mimev = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("mime")); 151 | const char *mime = getstring(mimev, "text/plain"); 152 | const char *filepath; 153 | if (!janet_checktype(filev, JANET_STRING)) { 154 | mg_send_head(c, 500, 0, "expected string :file option to serve a file"); 155 | break; 156 | } 157 | filepath = getstring(filev, ""); 158 | mg_http_serve_file(c, (struct http_message *)ev_data, filepath, mg_mk_str(mime), mg_mk_str("")); 159 | return; 160 | } 161 | } 162 | 163 | /* Serve a generic HTTP response */ 164 | 165 | Janet status = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("status")); 166 | Janet headers = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("headers")); 167 | Janet body = janet_dictionary_get(kvs, kvcap, janet_ckeywordv("body")); 168 | 169 | int code; 170 | if (janet_checktype(status, JANET_NIL)) 171 | code = 200; 172 | else if (janet_checkint(status)) 173 | code = janet_unwrap_integer(status); 174 | else 175 | break; 176 | 177 | const JanetKV *headerkvs; 178 | int32_t headerlen, headercap; 179 | if (janet_checktype(headers, JANET_NIL)) { 180 | headerkvs = NULL; 181 | headerlen = 0; 182 | headercap = 0; 183 | } else if (!janet_dictionary_view(headers, &headerkvs, &headerlen, &headercap)) { 184 | break; 185 | } 186 | 187 | const uint8_t *bodybytes; 188 | int32_t bodylen; 189 | if (janet_checktype(body, JANET_NIL)) { 190 | bodybytes = NULL; 191 | bodylen = 0; 192 | } else if (!janet_bytes_view(body, &bodybytes, &bodylen)) { 193 | break; 194 | } 195 | 196 | mg_send_response_line(c, code, NULL); 197 | for (const JanetKV *kv = janet_dictionary_next(headerkvs, headercap, NULL); 198 | kv; 199 | kv = janet_dictionary_next(headerkvs, headercap, kv)) { 200 | const uint8_t *name = janet_to_string(kv->key); 201 | int32_t header_len; 202 | const Janet *header_items; 203 | if (janet_indexed_view(kv->value, &header_items, &header_len)) { 204 | /* Array-like of headers */ 205 | for (int32_t i = 0; i < header_len; i++) { 206 | const uint8_t *value = janet_to_string(header_items[i]); 207 | mg_printf(c, "%s: %s\r\n", (const char *)name, (const char *)value); 208 | } 209 | } else { 210 | /* Single header */ 211 | const uint8_t *value = janet_to_string(kv->value); 212 | mg_printf(c, "%s: %s\r\n", (const char *)name, (const char *)value); 213 | } 214 | } 215 | 216 | mg_printf(c, "Content-Length: %d\r\n", bodylen); 217 | mg_printf(c, "\r\n"); 218 | if (bodylen) mg_send(c, bodybytes, bodylen); 219 | } 220 | break; 221 | } 222 | mg_printf(c, "\r\n"); 223 | c->flags |= MG_F_SEND_AND_CLOSE; 224 | } 225 | 226 | /* The dispatching event handler. This handler is what 227 | * is presented to mongoose, but it dispatches to dynamically 228 | * defined handlers. */ 229 | static void http_handler(struct mg_connection *c, int ev, void *p) { 230 | Janet evdata; 231 | switch (ev) { 232 | default: 233 | return; 234 | case MG_EV_HTTP_REQUEST: 235 | evdata = build_http_request(c, (struct http_message *)p); 236 | break; 237 | } 238 | ConnectionWrapper *cw; 239 | JanetFiber *fiber; 240 | cw = (ConnectionWrapper *)(c->user_data); 241 | fiber = cw->fiber; 242 | Janet out; 243 | JanetSignal status = janet_continue(fiber, evdata, &out); 244 | if (status != JANET_SIGNAL_OK && status != JANET_SIGNAL_YIELD) { 245 | janet_stacktrace(fiber, out); 246 | return; 247 | } 248 | send_http(c, out, p); 249 | } 250 | 251 | static Janet cfun_manager(int32_t argc, Janet *argv) { 252 | janet_fixarity(argc, 0); 253 | (void) argv; 254 | void *mgr = janet_abstract(&Manager_jt, sizeof(struct mg_mgr)); 255 | mg_mgr_init(mgr, NULL); 256 | return janet_wrap_abstract(mgr); 257 | } 258 | 259 | /* Common functionality for binding */ 260 | static void do_bind(int32_t argc, Janet *argv, struct mg_connection **connout, 261 | void (*handler)(struct mg_connection *, int, void *)) { 262 | janet_fixarity(argc, 3); 263 | 264 | /* We use opts, so that we can read the error reason from mongoose if bind fails. 265 | As described here https://github.com/cesanta/mongoose/issues/983 */ 266 | struct mg_bind_opts opts; 267 | memset(&opts, 0, sizeof(opts)); 268 | const char *err = NULL; 269 | opts.error_string = &err; 270 | 271 | struct mg_mgr *mgr = janet_getabstract(argv, 0, &Manager_jt); 272 | const uint8_t *port = janet_getstring(argv, 1); 273 | JanetFunction *onConnection = janet_getfunction(argv, 2); 274 | struct mg_connection *conn = mg_bind_opt(mgr, (const char *)port, handler, opts); 275 | if (NULL == conn) { 276 | janet_panicf("could not bind to %s, reason being: %s", port, err); 277 | } 278 | JanetFiber *fiber = janet_fiber(onConnection, 64, 0, NULL); 279 | ConnectionWrapper *cw = janet_abstract(&Connection_jt, sizeof(ConnectionWrapper)); 280 | cw->conn = conn; 281 | cw->fiber = fiber; 282 | conn->user_data = cw; 283 | Janet out; 284 | JanetSignal status = janet_continue(fiber, janet_wrap_abstract(cw), &out); 285 | if (status != JANET_SIGNAL_YIELD) { 286 | janet_stacktrace(fiber, out); 287 | } 288 | *connout = conn; 289 | } 290 | 291 | static Janet cfun_bind_http(int32_t argc, Janet *argv) { 292 | struct mg_connection *conn = NULL; 293 | do_bind(argc, argv, &conn, http_handler); 294 | mg_set_protocol_http_websocket(conn); 295 | return argv[0]; 296 | } 297 | 298 | 299 | 300 | static int is_websocket(const struct mg_connection *nc) { 301 | return nc->flags & MG_F_IS_WEBSOCKET; 302 | } 303 | 304 | static Janet build_websocket_event(struct mg_connection *c, Janet event, struct websocket_message *wm) { 305 | JanetTable *payload; 306 | if (wm) { 307 | payload = janet_table(4); 308 | janet_table_put(payload, janet_ckeywordv("data"), janet_stringv((const uint8_t *) wm->data, wm->size)); 309 | } else { 310 | payload = janet_table(3); 311 | } 312 | 313 | janet_table_put(payload, janet_ckeywordv("event"), event); 314 | janet_table_put(payload, janet_ckeywordv("protocol"), janet_cstringv("websocket")); 315 | janet_table_put(payload, janet_ckeywordv("connection"), janet_wrap_abstract(c->user_data)); 316 | return janet_wrap_table(payload); 317 | } 318 | 319 | /* The dispatching event handler. This handler is what 320 | * is presented to mongoose, but it dispatches to dynamically 321 | * defined handlers. */ 322 | static void http_websocket_handler(struct mg_connection *c, int ev, void *p) { 323 | Janet evdata; 324 | 325 | switch (ev) { 326 | default: 327 | return; 328 | 329 | case MG_EV_HTTP_REQUEST: { 330 | http_handler(c, ev, p); 331 | return; 332 | } 333 | 334 | case MG_EV_WEBSOCKET_HANDSHAKE_DONE: { 335 | evdata = build_websocket_event(c, janet_ckeywordv("open"), NULL); 336 | break; 337 | } 338 | 339 | case MG_EV_WEBSOCKET_FRAME: { 340 | struct websocket_message *wm = (struct websocket_message *) p; 341 | evdata = build_websocket_event(c, janet_ckeywordv("message"), wm); 342 | break; 343 | } 344 | 345 | case MG_EV_CLOSE: { 346 | evdata = build_websocket_event(c, janet_ckeywordv("close"), NULL); 347 | break; 348 | } 349 | 350 | } 351 | 352 | ConnectionWrapper *cw; 353 | JanetFiber *fiber; 354 | cw = (ConnectionWrapper *)(c->user_data); 355 | fiber = cw->fiber; 356 | Janet out; 357 | JanetSignal status = janet_continue(fiber, evdata, &out); 358 | if (status != JANET_SIGNAL_OK && status != JANET_SIGNAL_YIELD) { 359 | janet_stacktrace(fiber, out); 360 | return; 361 | } 362 | } 363 | 364 | static Janet cfun_bind_http_websocket(int32_t argc, Janet *argv) { 365 | struct mg_connection *conn = NULL; 366 | do_bind(argc, argv, &conn, http_websocket_handler); 367 | mg_set_protocol_http_websocket(conn); 368 | return argv[0]; 369 | } 370 | 371 | static Janet cfun_broadcast(int32_t argc, Janet *argv) { 372 | janet_fixarity(argc, 2); 373 | struct mg_mgr *mgr = janet_getabstract(argv, 0, &Manager_jt); 374 | const char *buf = janet_getcstring(argv, 1); 375 | struct mg_connection *c; 376 | for (c = mg_next(mgr, NULL); c != NULL; c = mg_next(mgr, c)) { 377 | mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, buf, strlen(buf)); 378 | } 379 | 380 | return argv[0]; 381 | } 382 | 383 | static const JanetReg cfuns[] = { 384 | {"manager", cfun_manager, NULL}, 385 | {"poll", cfun_poll, NULL}, 386 | {"bind-http", cfun_bind_http, NULL}, 387 | {"broadcast", cfun_broadcast, NULL}, 388 | {"bind-http-websocket", cfun_bind_http_websocket, NULL}, 389 | {NULL, NULL, NULL} 390 | }; 391 | 392 | extern const unsigned char *circlet_lib_embed; 393 | extern size_t circlet_lib_embed_size; 394 | 395 | JANET_MODULE_ENTRY(JanetTable *env) { 396 | janet_cfuns(env, "circlet", cfuns); 397 | janet_dobytes(env, 398 | circlet_lib_embed, 399 | circlet_lib_embed_size, 400 | "circlet_lib.janet", 401 | NULL); 402 | } 403 | -------------------------------------------------------------------------------- /circlet_lib.janet: -------------------------------------------------------------------------------- 1 | # This is embedded, so all circlet functions are available 2 | 3 | (defn middleware 4 | "Coerce any type to http middleware" 5 | [x] 6 | (case (type x) 7 | :function x 8 | (fn [&] x))) 9 | 10 | (defn router 11 | "Creates a router middleware. Route parameter must be table or struct 12 | where keys are URI paths and values are handler functions for given URI" 13 | [routes] 14 | (fn [req] 15 | (def r (or 16 | (get routes (get req :uri)) 17 | (get routes :default))) 18 | (if r ((middleware r) req) 404))) 19 | 20 | (defn logger 21 | "Creates a logging middleware. nextmw parameter is the handler function 22 | of the next middleware" 23 | [nextmw] 24 | (fn [req] 25 | (def {:uri uri 26 | :protocol proto 27 | :method method 28 | :query-string qs} req) 29 | (def start-clock (os/clock)) 30 | (def ret (nextmw req)) 31 | (def end-clock (os/clock)) 32 | (def fulluri (if (< 0 (length qs)) (string uri "?" qs) uri)) 33 | (def elapsed (string/format "%.3f" (* 1000 (- end-clock start-clock)))) 34 | (def status (or (get ret :status) 200)) 35 | (print proto " " method " " status " " fulluri " elapsed " elapsed "ms") 36 | ret)) 37 | 38 | (defn cookies 39 | "Parses cookies into the table under :cookies key. nextmw parameter is 40 | the handler function of the next middleware" 41 | [nextmw] 42 | (def grammar 43 | (peg/compile 44 | {:content '(some (if-not (set "=;") 1)) 45 | :eql "=" 46 | :sep '(between 1 2 (set "; ")) 47 | :main '(some (* (<- :content) :eql (<- :content) (? :sep)))})) 48 | (fn [req] 49 | (-> req 50 | (put :cookies 51 | (or (-?>> [:headers "Cookie"] 52 | (get-in req) 53 | (peg/match grammar) 54 | (apply table)) 55 | {})) 56 | nextmw))) 57 | 58 | (defn server 59 | "Creates a simple http server. handler parameter is the function handling the 60 | requests. It could be middleware. port is the number of the port the server 61 | will listen on. ip-address is optional IP address the server will listen on" 62 | [handler port &opt ip-address] 63 | (def mgr (manager)) 64 | (def mw (middleware handler)) 65 | (default ip-address "127.0.0.1") 66 | (def interface 67 | (if (peg/match "*" ip-address) 68 | (string port) 69 | (string/format "%s:%d" ip-address port))) 70 | (defn evloop [] 71 | (print (string/format "Circlet server listening on [%s:%d] ..." ip-address port)) 72 | (var req (yield nil)) 73 | (while true 74 | (set req (yield (mw req))))) 75 | (bind-http mgr interface evloop) 76 | (while true (poll mgr 2000))) 77 | 78 | 79 | (defn server-websocket 80 | "Creates a simple http+websocket server. handler parameter is the function handling the 81 | requests. It could be middleware. websocket-handler is the function handling websocket 82 | messages. port is the number of the port the server 83 | will listen on. ip-address is optional IP address the server will listen on" 84 | [handler websocket-handler port &opt ip-address] 85 | (def mgr (manager)) 86 | (def mw (middleware handler)) 87 | (def ws-mw (middleware websocket-handler)) 88 | (default ip-address "127.0.0.1") 89 | (def interface 90 | (if (peg/match "*" ip-address) 91 | (string port) 92 | (string/format "%s:%d" ip-address port))) 93 | (defn evloop [] 94 | (print (string/format "Circlet server listening on [%s:%d] ..." ip-address port)) 95 | (var req (yield nil)) 96 | (while true 97 | (case (req :protocol) 98 | "websocket" 99 | (set req (yield (ws-mw mgr req))) 100 | 101 | (set req (yield (mw req)))))) 102 | (bind-http-websocket mgr interface evloop) 103 | (while true (poll mgr 2000))) 104 | -------------------------------------------------------------------------------- /project.janet: -------------------------------------------------------------------------------- 1 | (declare-project 2 | :name "circlet" 3 | :description "HTTP server bindings for Janet." 4 | :author "Calvin Rose" 5 | :license "MIT" 6 | :url "https://github.com/janet-lang/circlet" 7 | :repo "git+https://github.com/janet-lang/circlet.git") 8 | 9 | (declare-native 10 | :name "circlet" 11 | :embedded ["circlet_lib.janet"] 12 | :lflags (if (= :windows (os/which)) 13 | # for now, assume 32 bit compilation. 14 | ["advapi32.lib"]) 15 | :source ["circlet.c" "mongoose.c"]) 16 | 17 | (phony "update-mongoose" [] 18 | (os/shell "curl https://raw.githubusercontent.com/cesanta/mongoose/master/mongoose.c > mongoose.c") 19 | (os/shell "curl https://raw.githubusercontent.com/cesanta/mongoose/master/mongoose.h > mongoose.h")) 20 | -------------------------------------------------------------------------------- /test/testserv.janet: -------------------------------------------------------------------------------- 1 | (import build/circlet :as circlet) 2 | 3 | # Now build our server 4 | (circlet/server 5 | (-> 6 | {"/thing" {:status 200 7 | :headers {"Content-Type" "text/html; charset=utf-8" 8 | "Thang" [1 2 3 4 5]} 9 | :body " 10 |

Is a thing.

11 |
12 | 13 | 14 |
15 | "} 16 | "/bork" (fn [req] 17 | (let [[fname] (peg/match '(* "firstname=" (<- (any 1)) -1) (req :query-string))] 18 | {:status 200 :body (string "Your firstname is " 19 | fname "?")})) 20 | "/blob" {:status 200 21 | :body @"123\0123"} 22 | "/redirect" {:status 302 23 | :headers {"Location" "/thing"}} 24 | "/readme" {:kind :file :file "README.md" :mime "text/plain"} 25 | :default {:kind :static 26 | :root "."}} 27 | circlet/router 28 | circlet/logger) 29 | 8000) 30 | --------------------------------------------------------------------------------