├── .drone.yml ├── .gitignore ├── Makefile ├── README.md ├── conanfile.py ├── src ├── CATS │ └── ats-http.cats ├── DATS │ ├── connection.dats │ ├── headers.dats │ ├── http.dats │ ├── request.dats │ └── response.dats ├── HATS │ └── includes.hats ├── SATS │ ├── connection.sats │ ├── headers.sats │ ├── http.sats │ ├── request.sats │ ├── response.sats │ └── types.sats └── ats-http.hats └── tests ├── Makefile ├── conanfile.py ├── index.html ├── test.db └── tests.dats /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | steps: 5 | - name: build 6 | image: xrandeex/ats2:0.4.2 7 | commands: 8 | - conan remote add pkg https://pkg.us.to 9 | - conan install . -if build 10 | - conan build . -if build 11 | - name: test 12 | image: xrandeex/ats2:0.4.2 13 | commands: 14 | - conan remote add pkg https://pkg.us.to 15 | - cd tests 16 | - conan install . -if build 17 | - conan build . -if build 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.c 2 | build 3 | node_modules 4 | target 5 | .build 6 | .libs 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ATSCC=$(PATSHOME)/bin/patscc 2 | ATSOPT=$(PATSHOME)/bin/patsopt 3 | 4 | ATSFLAGS+=-IATS src 5 | 6 | CFLAGS+=-DATS_MEMALLOC_LIBC -D_DEFAULT_SOURCE -I $(PATSHOME)/ccomp/runtime -I $(PATSHOME) -O3 -I src 7 | LDFLAGS+=-L $(PATSHOME)/ccomp/atslib/lib 8 | LIBS+=-latslib 9 | 10 | NAME := libats-http 11 | SNAME := $(NAME).a 12 | DNAME := $(NAME).so 13 | SRCDIR := src 14 | vpath %.dats src 15 | vpath %.dats src/DATS 16 | vpath %.sats src/SATS 17 | SRCS := $(shell find $(SRCDIR) -name '*.dats' -type f -exec basename {} \;) 18 | SDIR := build-static 19 | SOBJ := $(patsubst %.dats,$(SDIR)/%.o,$(SRCS)) 20 | DDIR := build-shared 21 | DOBJ := $(patsubst %.dats,$(DDIR)/%.o,$(SRCS)) 22 | 23 | .PHONY: all clean fclean re 24 | 25 | all: $(SNAME) $(DNAME) 26 | 27 | $(SNAME): $(SOBJ) 28 | $(AR) $(ARFLAGS) $@ $^ 29 | 30 | $(DNAME): CFLAGS += -fPIC 31 | $(DNAME): LDFLAGS += -shared 32 | $(DNAME): $(DOBJ) 33 | $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ 34 | 35 | $(SDIR)/%.o: %.c | $(SDIR) 36 | $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $< 37 | 38 | $(DDIR)/%.o: %.c | $(DDIR) 39 | $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $< 40 | 41 | %.c: %.dats 42 | $(ATSOPT) $(ATSFLAGS) -o $(@F) -d $< 43 | 44 | $(SDIR) $(DDIR): 45 | @mkdir $@ 46 | 47 | clean: 48 | $(RM) -r $(SDIR) $(DDIR) 49 | 50 | fclean: clean 51 | $(RM) $(SNAME) $(DNAME) 52 | 53 | re: fclean all 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://cloud.drone.io/api/badges/xran-deex/ats-http/status.svg)](https://cloud.drone.io/xran-deex/ats-http) 2 | 3 | # ats-http 4 | 5 | ## Quick start (Docker) 6 | ```bash 7 | docker run -v `pwd`:/src --net=host -it --rm -e CONAN_REMOTE= xrandeex/ats2:0.4.2 "conan install . -if build && conan build . -if build" 8 | ``` 9 | 10 | ## Dependencies 11 | Dependencies are handled using conan (which depends on Python) 12 | Conan https://conan.io 13 | 14 | A simple conan helper file needs to be installed to add the correct include paths to the makefile. (https://github.com/xran-deex/atsconan) 15 | 16 | ## Example 17 | ``` ats 18 | #include "../ats-http.hats" 19 | staload "libats/libc/SATS/string.sats" 20 | 21 | staload $REQ 22 | staload $RESP 23 | 24 | implement main(argc, argv) = 0 where { 25 | // make the server 26 | var server = make_server(8888) 27 | // use 6 threads 28 | val () = set_thread_count(server, 6) 29 | 30 | // setup a get response handler 31 | val () = get(server, "/hello", lam (req,resp) = copy("Hello World") where { 32 | val () = set_status_code(resp, 200) 33 | val () = set_content_type(resp, "text/plain") 34 | }) 35 | 36 | // run the server 37 | val () = run_server(server) 38 | val () = free_server(server) 39 | } 40 | ``` 41 | ## Install dependencies 42 | ``` bash 43 | conan install . --install-folder build 44 | ``` 45 | 46 | ## Build 47 | ``` bash 48 | conan build . --install-folder build 49 | ``` 50 | 51 | ## Build and run test app 52 | ``` bash 53 | 54 | cd tests 55 | conan install . --install-folder build 56 | conan build . --install-folder build 57 | ./tests 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from atsconan import ATSConan 2 | 3 | class ATSConan(ATSConan): 4 | name = "ats-http" 5 | version = "0.2" 6 | requires = [ 7 | "ats-threadpool/0.1@randy.valis/testing", 8 | "ats-epoll/0.2.1@randy.valis/testing", 9 | "ats-libz/0.1@randy.valis/testing", 10 | "hashtable-vt/0.2@randy.valis/testing", 11 | "ats-pthread-extensions/0.1@randy.valis/testing", 12 | "ats-shared-vt/0.1@randy.valis/testing" 13 | ] 14 | 15 | def package_info(self): 16 | super().package_info() 17 | self.cpp_info.libs = ["ats-http"] 18 | self.cpp_info.includedirs = ["src"] 19 | -------------------------------------------------------------------------------- /src/CATS/ats-http.cats: -------------------------------------------------------------------------------- 1 | #ifndef ATS_HTTP 2 | #define ATS_HTTP 3 | 4 | int http_write_err(int fd, void* buf, size_t n) { 5 | return write(fd, buf, n); 6 | } 7 | int http_read_err(int fd, void* buf, size_t n) { 8 | return read(fd, buf, n); 9 | } 10 | #include 11 | #include 12 | #include 13 | void reuseport(int fd) { 14 | int enable = 1; 15 | if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) 16 | printf("setsockopt(SO_REUSEADDR) failed\n"); 17 | setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); 18 | setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(int)); 19 | } 20 | #endif -------------------------------------------------------------------------------- /src/DATS/connection.dats: -------------------------------------------------------------------------------- 1 | #include "./../HATS/includes.hats" 2 | staload "./../SATS/connection.sats" 3 | staload "./../SATS/headers.sats" 4 | staload "./../SATS/response.sats" 5 | staload REQ = "./../SATS/request.sats" 6 | staload "./../SATS/types.sats" 7 | staload _ = "./../DATS/request.dats" 8 | staload _ = "./../DATS/response.dats" 9 | #define ATS_DYNLOADFLAG 0 10 | 11 | assume Conn = conn_ 12 | 13 | #define BUFSZ 2048 14 | 15 | fn isnotnewline(ch: char): bool = ch != '\n' && ch != '\r' 16 | 17 | fn method_from_string(s: strptr): Method = res where { 18 | val () = assertloc(strptr_isnot_null(s)) 19 | val ss = $UNSAFE.castvwtp1{string}(s) 20 | val res = (case+ 0 of 21 | | _ when s = "GET" => GET 22 | | _ when s = "POST" => POST 23 | | _ when s = "HEAD" => HEAD 24 | | _ when s = "PUT" => PUT 25 | | _ when s = "DELETE" => DELETE 26 | | _ => $raise GenerallyExn("Unable to parse method")): Method 27 | val () = free(s) 28 | } 29 | 30 | implement method_to_string(m) = res where { 31 | val res = case+ m of 32 | | GET() => "GET" 33 | | POST() => "POST" 34 | | PUT() => "PUT" 35 | | HEAD() => "HEAD" 36 | | DELETE() => "DELETE" 37 | } 38 | 39 | implement make_conn(fd) = conn where { 40 | val conn = C(_) 41 | val C(c) = conn 42 | val () = c.fd := fd 43 | val () = c.req := stringbuf_make_nil_int(1024) 44 | val () = c.res := stringbuf_make_nil_int(1024) 45 | val () = c.request := $REQ.make_empty_req() 46 | val () = c.status := 200 47 | val () = c.response := make_response() 48 | prval() = fold@conn 49 | } 50 | 51 | implement set_status(conn, status) = { 52 | val+@C(c) = conn 53 | val () = c.status := status 54 | prval () = fold@conn 55 | } 56 | 57 | implement free_conn(conn) = { 58 | val () = case+ conn of 59 | | ~Some_vt(conn) => { 60 | val+~C(c) = conn 61 | val () = stringbuf_free(c.req) 62 | val () = stringbuf_free(c.res) 63 | val () = $REQ.free_request(c.request) 64 | val () = free_response(c.response) 65 | } 66 | | ~None_vt() => () 67 | } 68 | 69 | implement append_data(conn, buf, s, cnt) = { 70 | val+@C(c) = conn 71 | vtypedef state = @{ b=stringbuf, i = int } 72 | var e: state = @{ b=c.req, i = 0 } 73 | val _ = array_foreach_env(buf, i2sz s, e) where { 74 | implement array_foreach$fwork(b, ev) = { 75 | val ch = $UNSAFE.cast{int} b 76 | val ch = int2char0 ch 77 | val [n:int] ch = g1ofg0(ch) 78 | val () = if ev.i < cnt then if char1_isneqz(ch) then { 79 | val _ = stringbuf_insert_char(ev.b, ch) 80 | } 81 | val () = ev.i := ev.i + 1 82 | } 83 | } 84 | val () = c.req := e.b 85 | prval () = fold@conn 86 | } 87 | 88 | implement parse_conn_from_buffer(conn, buf, s) = { 89 | val+@C(c) = conn 90 | val sb = stringbuf_make_nil_int(1024) 91 | datavtype parse_state = | METHOD | PATH | PROTO | HEADERKEY | HEADERVALUE | HEADERSPACE | NEWLINE | BODY | DONE 92 | vtypedef state = @{ b=stringbuf, s=parse_state, request=Req, headerkey=strptr } 93 | var e: state = @{ b=sb, s=METHOD, request=c.request, headerkey=copy("") } 94 | val _ = array_foreach_env(buf, i2sz s, e) where { 95 | implement array_foreach$fwork(b, ev) = { 96 | val ch = $UNSAFE.cast{int} b 97 | val ch = int2char0 ch 98 | val [n:int] ch = g1ofg0(ch) 99 | val () = case+ ev.s of 100 | | ~METHOD() => { 101 | val () = if isspace(ch) then { 102 | val () = $REQ.set_method(ev.request, method_from_string(stringbuf_truncout_all(ev.b))) 103 | val () = ev.s := PATH 104 | } else { 105 | val () = ev.s := METHOD 106 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 107 | } 108 | 109 | } 110 | | ~PATH() => { 111 | val () = if isspace(ch) then { 112 | val () = $REQ.set_path(ev.request, stringbuf_truncout_all(ev.b)) 113 | val () = ev.s := PROTO 114 | } else { 115 | val () = ev.s := PATH 116 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 117 | } 118 | } 119 | | ~PROTO() => { 120 | val () = if ch = '\r' then { 121 | val () = free(stringbuf_truncout_all(ev.b)) 122 | val () = ev.s := NEWLINE 123 | } else { 124 | val () = ev.s := PROTO 125 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 126 | } 127 | } 128 | | ~HEADERKEY() => { 129 | val () = if ch = ':' then { 130 | val () = ev.s := HEADERSPACE 131 | val line = stringbuf_truncout_all(ev.b) 132 | val () = free(ev.headerkey) 133 | val () = ev.headerkey := line 134 | } else if ch = '\r' then { 135 | val () = ev.s := BODY 136 | } else { 137 | val () = ev.s := HEADERKEY 138 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 139 | } 140 | } 141 | | ~HEADERSPACE() => { 142 | val () = ev.s := HEADERVALUE 143 | } 144 | | ~HEADERVALUE() => { 145 | val () = if ch = '\r' then { 146 | val () = ev.s := NEWLINE 147 | val line = stringbuf_truncout_all(ev.b) 148 | val key = ev.headerkey 149 | val () = $REQ.add_header_value(ev.request, key, line) 150 | val () = ev.headerkey := copy("") 151 | } else { 152 | val () = ev.s := HEADERVALUE 153 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 154 | } 155 | } 156 | | ~NEWLINE() => { 157 | val () = ev.s := HEADERKEY 158 | } 159 | | ~BODY() => { 160 | val () = if ch = '\n' then { 161 | val () = ev.s := BODY 162 | } else if char1_iseqz(ch) then { 163 | val () = ev.s := DONE 164 | val () = $REQ.set_body(ev.request, stringbuf_truncout_all(ev.b)) 165 | } else { 166 | val () = ev.s := BODY 167 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 168 | } 169 | } 170 | | ~DONE() => { 171 | val () = ev.s := DONE 172 | } 173 | } 174 | } 175 | val () = stringbuf_free(e.b) 176 | val () = case+ e.s of | ~METHOD() => () | ~PATH() => () | ~PROTO() => () | ~HEADERKEY() => () | ~HEADERVALUE() => () | ~HEADERSPACE() => () | ~NEWLINE() => () | ~BODY() => () | ~DONE() => () 177 | val () = c.request := e.request 178 | val () = free(e.headerkey) 179 | prval () = fold@conn 180 | } 181 | 182 | implement parse_conn(conn) = { 183 | val+@C(c) = conn 184 | val req = stringbuf_truncout_all(c.req) 185 | val req = strptr2strnptr(req) 186 | val () = assertloc(strnptr_length(req) >= 0) 187 | val sb = stringbuf_make_nil_int(1024) 188 | datavtype parse_state = | METHOD | PATH | PROTO | HEADERKEY | HEADERVALUE | HEADERSPACE | NEWLINE | BODY | DONE 189 | vtypedef state = @{ b=stringbuf, s=parse_state, request=Req, headerkey=strptr } 190 | var e: state = @{ b=sb, s=METHOD, request=c.request, headerkey=copy("") } 191 | val _ = strnptr_foreach_env(req, e) where { 192 | implement strnptr_foreach$fwork(ch, ev) = { 193 | val [n:int] ch = g1ofg0(ch) 194 | val () = case+ ev.s of 195 | | ~METHOD() => { 196 | val () = if isspace(ch) then { 197 | val () = $REQ.set_method(ev.request, method_from_string(stringbuf_truncout_all(ev.b))) 198 | val () = ev.s := PATH 199 | } else { 200 | val () = ev.s := METHOD 201 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 202 | } 203 | 204 | } 205 | | ~PATH() => { 206 | val () = if isspace(ch) then { 207 | val () = $REQ.set_path(ev.request, stringbuf_truncout_all(ev.b)) 208 | val () = ev.s := PROTO 209 | } else { 210 | val () = ev.s := PATH 211 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 212 | } 213 | } 214 | | ~PROTO() => { 215 | val () = if ch = '\r' then { 216 | val () = free(stringbuf_truncout_all(ev.b)) 217 | val () = ev.s := NEWLINE 218 | } else { 219 | val () = ev.s := PROTO 220 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 221 | } 222 | } 223 | | ~HEADERKEY() => { 224 | val () = if ch = ':' then { 225 | val () = ev.s := HEADERSPACE 226 | val line = stringbuf_truncout_all(ev.b) 227 | val () = free(ev.headerkey) 228 | val () = ev.headerkey := line 229 | } else if ch = '\r' then { 230 | val () = ev.s := BODY 231 | } else { 232 | val () = ev.s := HEADERKEY 233 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 234 | } 235 | } 236 | | ~HEADERSPACE() => { 237 | val () = ev.s := HEADERVALUE 238 | } 239 | | ~HEADERVALUE() => { 240 | val () = if ch = '\r' then { 241 | val () = ev.s := NEWLINE 242 | val line = stringbuf_truncout_all(ev.b) 243 | val key = ev.headerkey 244 | val () = $REQ.add_header_value(ev.request, key, line) 245 | val () = ev.headerkey := copy("") 246 | } else { 247 | val () = ev.s := HEADERVALUE 248 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 249 | } 250 | } 251 | | ~NEWLINE() => { 252 | val () = ev.s := HEADERKEY 253 | } 254 | | ~BODY() => { 255 | val () = if ch = '\n' then { 256 | val () = ev.s := BODY 257 | } else if char1_iseqz(ch) then { 258 | val () = ev.s := DONE 259 | val () = $REQ.set_body(ev.request, stringbuf_truncout_all(ev.b)) 260 | } else { 261 | val () = ev.s := BODY 262 | val _ = if char1_isneqz(ch) then stringbuf_insert_char(ev.b, ch) else 0 263 | } 264 | } 265 | | ~DONE() => { 266 | val () = ev.s := DONE 267 | } 268 | } 269 | } 270 | val () = free(req) 271 | val () = stringbuf_free(e.b) 272 | val () = case+ e.s of | ~METHOD() => () | ~PATH() => () | ~PROTO() => () | ~HEADERKEY() => () | ~HEADERVALUE() => () | ~HEADERSPACE() => () | ~NEWLINE() => () | ~BODY() => () | ~DONE() => () 273 | val () = c.request := e.request 274 | val () = free(e.headerkey) 275 | prval () = fold@conn 276 | } 277 | 278 | implement clear_request_buffer(conn) = { 279 | val+@C(c) = conn 280 | val () = free(stringbuf_takeout_all(c.req)) 281 | prval () = fold@conn 282 | } 283 | 284 | implement clear_response_buffer(conn) = { 285 | val+@C(c) = conn 286 | val () = free(stringbuf_takeout_all(c.res)) 287 | prval () = fold@conn 288 | } 289 | 290 | implement call_handler(conn, func) = res where { 291 | val+@C(c) = conn 292 | val res = func(c.request, c.response) 293 | prval () = $UNSAFE.cast2void(func) 294 | prval () = fold@conn 295 | } 296 | 297 | implement get_routing_key(conn) = key where { 298 | val+@C(c) = conn 299 | val key = string0_append(method_to_string($REQ.get_method(c.request)), $REQ.get_path(c.request)) 300 | prval () = fold@conn 301 | } 302 | 303 | fn add_content_type(sb: !stringbuf, ty: string): int = let 304 | val _ = stringbuf_insert_string(sb, "Content-Type: ") 305 | val _ = stringbuf_insert_string(sb, ty) 306 | in 307 | stringbuf_insert_string(sb, "\r\n") 308 | end 309 | 310 | fn add_keep_alive(sb: !stringbuf): int = 311 | stringbuf_insert_string(sb, "Connection: Keep-Alive\r\n") 312 | 313 | fn add_http_1_1(sb: !stringbuf): int = 314 | stringbuf_insert_string(sb, "HTTP/1.1 ") 315 | 316 | fn add_200(sb: !stringbuf): int = 317 | stringbuf_insert_string(sb, "200 OK\r\n") 318 | 319 | fn add_status_code(sb: !stringbuf, code: int): int = res where { 320 | val res = stringbuf_insert_int(sb, code) 321 | val res = case+ code of 322 | | 200 => stringbuf_insert_string(sb, " OK\r\n") 323 | | 404 => stringbuf_insert_string(sb, " NOT FOUND\r\n") 324 | | 500 => stringbuf_insert_string(sb, " INTERNAL SERVER ERROR\r\n") 325 | | _ => stringbuf_insert_string(sb, " UNKNOWN\r\n") 326 | } 327 | 328 | fn add_gzip(sb: !stringbuf): int = 329 | stringbuf_insert_string(sb, "Content-Encoding: deflate\r\n") 330 | 331 | fn add_content_length(sb: !stringbuf, content: !strptr): int = let 332 | val contentLen = strptr_length(content) 333 | val _ = stringbuf_insert_string(sb, "Content-Length: ") 334 | val _ = stringbuf_insert_int(sb, $UNSAFE.cast{int}contentLen) 335 | in 336 | stringbuf_insert_string(sb, "\r\n") 337 | end 338 | 339 | fn add_content_length_from_int(sb: !stringbuf, contentLen: int): int = let 340 | val _ = stringbuf_insert_string(sb, "Content-Length: ") 341 | val _ = stringbuf_insert_int(sb, contentLen) 342 | in 343 | stringbuf_insert_string(sb, "\r\n") 344 | end 345 | 346 | fn finish_headers(sb: !stringbuf): int = 347 | stringbuf_insert_string(sb, "\r\n") 348 | 349 | fn add_content(sb: !stringbuf, content: strptr): void = let 350 | val _ = stringbuf_insert_string(sb, $UNSAFE.castvwtp1{string}(content)) 351 | in 352 | free(content) 353 | end 354 | 355 | fn add_content_gzip(sb: !stringbuf, content: !strptr, sz: int): void = { 356 | val [n:int] size = $UNSAFE.cast{[n:int]size_t(n)}(sz) 357 | val c = $UNSAFE.castvwtp1{string(n)}(content) 358 | val _ = stringbuf_insert_strlen(sb, c, size) 359 | } 360 | 361 | fn compress_content(content: strptr, compressed: !ptr, sz: int): int = res where { 362 | var destLen: lint = g0int2int_int_lint BUFSZ 363 | val result = $LIBZ.compress(compressed, destLen, $UNSAFE.castvwtp1{ptr}content, g0int2int_int_lint sz) 364 | val () = free(content) 365 | val res = $UNSAFE.cast{int}destLen 366 | } 367 | 368 | implement create_response(conn, res) = { 369 | val+@C(c) = conn 370 | val _ = add_http_1_1(c.res) 371 | val _ = add_status_code(c.res, get_status_code(c.response)) 372 | val _ = add_content_type(c.res, get_content_type(c.response)) 373 | val _ = add_keep_alive(c.res) 374 | val _ = add_content_length(c.res, res) 375 | val _ = finish_headers(c.res) 376 | val () = add_content(c.res, res) 377 | prval () = fold@conn 378 | } 379 | 380 | implement create_response_gzip(conn, res) = { 381 | val+@C(c) = conn 382 | val _ = add_http_1_1(c.res) 383 | val _ = add_status_code(c.res, get_status_code(c.response)) 384 | val _ = add_content_type(c.res, get_content_type(c.response)) 385 | val _ = add_keep_alive(c.res) 386 | val _ = add_gzip(c.res) 387 | var bufs = @[byte][1024](int2byte0 0) 388 | val cnt = compress_content(res, $UNSAFE.cast{ptr}bufs, $UNSAFE.cast{int}(strptr_length(res))) 389 | val str = $UNSAFE.castvwtp1{strptr}(bufs) 390 | val _ = add_content_length_from_int(c.res, cnt) 391 | val _ = finish_headers(c.res) 392 | val () = add_content_gzip(c.res, str, cnt) 393 | // stack allocated byte array 394 | prval() = $UNSAFE.cast2void(str) 395 | prval () = fold@conn 396 | } 397 | 398 | implement get_buffer(conn, size) = res where { 399 | val+@C(c) = conn 400 | val res = stringbuf_takeout_strbuf(c.res, size) 401 | prval () = fold@conn 402 | } -------------------------------------------------------------------------------- /src/DATS/headers.dats: -------------------------------------------------------------------------------- 1 | #include "share/atspre_define.hats" 2 | #include "share/atspre_staload.hats" 3 | staload "./../SATS/headers.sats" 4 | #include "hashtable_vt.hats" 5 | staload "./../SATS/http.sats" 6 | staload "./../SATS/types.sats" 7 | #define ATS_DYNLOADFLAG 0 8 | 9 | assume Headers = headers_ 10 | 11 | implement parse_headers(buf) = headers where { 12 | val h = $HT.hashtbl_make_nil(i2sz 100) 13 | val-~None_vt() = $HT.hashtbl_insert_opt(h, copy("Connection"), copy("Keep-Alive")) 14 | val headers = H(@{ 15 | map = h 16 | }) 17 | } 18 | 19 | implement new_headers() = headers where { 20 | val h = $HT.hashtbl_make_nil(i2sz 100) 21 | val headers = H(@{ 22 | map = h 23 | }) 24 | } 25 | 26 | implement free_headers(headers) = { 27 | val+~H(h) = headers 28 | val () = $HT.hashtbl_free(h.map) where { 29 | implement $HT.hashtbl_free$clear(k, v) = { 30 | val () = free(k) 31 | val () = free(v) 32 | } 33 | } 34 | } 35 | 36 | implement get_header_value(headers, header) = value where { 37 | val+@H(h) = headers 38 | val key = $UNSAFE.castvwtp1{strptr} header 39 | val ptr = $HT.hashtbl_search_ref(h.map, key) 40 | prval() = $UNSAFE.cast2void(key) 41 | val value = (if ptr != 0 then Some_vt($UNSAFE.castvwtp0{string}($UNSAFE.p2tr_get(ptr))) 42 | else None_vt()): Option_vt(string) 43 | prval() = fold@headers 44 | } 45 | 46 | implement put_header_value(headers, key, value) = { 47 | val+@H(h) = headers 48 | val opt = $HT.hashtbl_takeout_opt(h.map, key) 49 | val () = case+ opt of 50 | | ~Some_vt(v) => { 51 | val () = free(v) 52 | val-~None_vt() = $HT.hashtbl_insert_opt(h.map, key, value) 53 | } 54 | | ~None_vt() => { 55 | val-~None_vt() = $HT.hashtbl_insert_opt(h.map, key, value) 56 | } 57 | prval() = fold@headers 58 | } 59 | 60 | implement print_headers(headers) = { 61 | val+@H(h) = headers 62 | val () = $HT.hashtbl_foreach(h.map) where { 63 | implement $HT.hashtbl_foreach$fwork(k, v, e) = { 64 | val () = println!(k, ": ", v) 65 | } 66 | } 67 | prval() = fold@headers 68 | } 69 | -------------------------------------------------------------------------------- /src/DATS/http.dats: -------------------------------------------------------------------------------- 1 | #include "./../HATS/includes.hats" 2 | staload "./../SATS/http.sats" 3 | staload "./../SATS/headers.sats" 4 | staload "./../SATS/types.sats" 5 | staload "./../SATS/request.sats" 6 | staload "./../SATS/response.sats" 7 | staload "./../SATS/connection.sats" 8 | staload _ = "./../DATS/connection.dats" 9 | #define ATS_DYNLOADFLAG 0 10 | #define ATS_PACKNAME "ats-http" 11 | 12 | staload $SHARED 13 | 14 | assume Server = shared(server_) 15 | 16 | fn get_server(port: int): [n:int|n>= ~1] int(n) = res where { 17 | val inport = in_port_nbo(port) 18 | val inaddr = in_addr_hbo2nbo (INADDR_ANY) 19 | // 20 | var servaddr: sockaddr_in_struct 21 | val () = sockaddr_in_init(servaddr, AF_INET, inaddr, inport) 22 | val (pf | sfd) = socket_AF_type_exn(AF_INET, SOCK_STREAM) 23 | val () = reuseport(sfd) 24 | val () = $extfcall(void, "atslib_libats_libc_bind_exn", sfd, addr@servaddr, socklen_in) 25 | prval() = __assert(pf) where { 26 | extern praxi __assert{fd:int}(pf: socket_v(fd,init)): void 27 | } 28 | val res = sfd 29 | } 30 | 31 | fn listen{n:int|n>=0}(sfd: int(n)): void = { 32 | val _ = setnonblocking(sfd) 33 | val () = $extfcall(void, "atslib_libats_libc_listen_exn", sfd, SOMAXCONN) 34 | } 35 | 36 | #define BUFSZ 1024 37 | 38 | fn write_response(conn: !Conn, fd: int): int = ret where { 39 | var size: size_t? 40 | val (pf, pff | buf) = get_buffer(conn, size) 41 | val () = assertloc(size >= 0) 42 | val () = assertloc(ptr_isnot_null buf) 43 | val ret = http_write_err(pf | fd, buf, size) 44 | val ret = $UNSAFE.cast{int}ret 45 | prval () = pff(pf) 46 | } 47 | 48 | fn get_handler(serve: !Server, conn: !Conn): Handler = handler where { 49 | val (pfs | server) = shared_lock(serve) 50 | val+@S(s) = server 51 | val key = get_routing_key(conn) 52 | val ptr = $HT.hashtbl_search_ref(s.router, key) 53 | val () = free(key) 54 | val handler = (if ptr != 0 then 55 | $UNSAFE.castvwtp1{Handler}($UNSAFE.p2tr_get(ptr)) 56 | else res where { 57 | // no handler for this path/method combo, so return 404 58 | val key = copy("NOTFOUND") 59 | val ptr = $HT.hashtbl_search_ref(s.router, key) 60 | val () = free(key) 61 | val res = (if ptr != 0 then $UNSAFE.castvwtp1{Handler}($UNSAFE.p2tr_get(ptr)) else $raise NotFoundExn): Handler 62 | } 63 | ): Handler 64 | prval () = fold@server 65 | 66 | val () = shared_unlock(pfs | serve, server) 67 | } 68 | 69 | fn set_response(serve: !Server, conn: !Conn, content: strptr): void = { 70 | val (pfs | server) = shared_lock(serve) 71 | val+@S(s) = server 72 | val () = if s.enable_gzip 73 | then 74 | create_response_gzip(conn, content) 75 | else 76 | create_response(conn, content) 77 | prval () = fold@server 78 | val () = shared_unlock(pfs | serve, server) 79 | } 80 | 81 | fun do_read(e: !Epoll(Server), w: !Watcher(Server, Conn), evs: uint): void = () where { 82 | val fd = watcher_get_fd(w) 83 | val isedge = (evs land EPOLLET) > 0 84 | val isread = (evs land EPOLLIN) > 0 85 | val iswrite = (evs land EPOLLOUT) > 0 86 | val iserr = (evs land EPOLLERR) > 0 87 | val isclose = (evs land (EPOLLRDHUP lor EPOLLHUP)) > 0 88 | val () = if isread && ~iserr then { 89 | fun loop(e: !Epoll(Server), w: !Watcher(Server, Conn)): void = { 90 | var buf with pf = @[byte][BUFSZ](int2byte0 0) 91 | val num_read = http_read_err(pf | fd, addr@buf, i2sz (BUFSZ)) 92 | 93 | // TODO - handle requests larger than the buffer size 94 | val () = if num_read >= 0 then { 95 | val (pf | opt) = watcher_data_takeout(w) 96 | val-@Some_vt(conn) = opt 97 | val () = parse_conn_from_buffer(conn, buf, BUFSZ) 98 | 99 | val (pf2 | opt2) = epoll_data_takeout(e) 100 | val-@Some_vt(serve) = opt2 101 | 102 | val () = clear_request_buffer(conn) 103 | 104 | val handler = get_handler(serve, conn) 105 | 106 | val res = call_handler(conn, handler) 107 | 108 | val () = set_response(serve, conn, res) 109 | prval () = fold@opt2 110 | prval () = fold@opt 111 | 112 | val () = watcher_data_addback(pf | w, opt) 113 | val () = update_watcher(e, w, EPOLLOUT lor EPOLLET) 114 | val () = epoll_data_addback(pf2 | e, opt2) 115 | } else if num_read > 0 then { 116 | val (pf | opt) = watcher_data_takeout(w) 117 | val-@Some_vt(conn) = opt 118 | val () = append_data(conn, buf, BUFSZ, num_read) 119 | prval () = fold@opt 120 | val () = watcher_data_addback(pf | w, opt) 121 | val () = loop(e, w) 122 | } 123 | } 124 | val () = loop(e, w) 125 | } 126 | 127 | val () = if iswrite && ~iserr then { 128 | fun loop(e: !Epoll(Server), w: !Watcher(Server, Conn)): void = { 129 | var buf = @[byte][BUFSZ](int2byte0 0) 130 | val (pf2 | opt) = watcher_data_takeout(w) 131 | val-@Some_vt(st) = opt 132 | 133 | val ret = write_response(st, fd) 134 | val () = clear_response_buffer(st) 135 | prval () = fold@opt 136 | 137 | val () = watcher_data_addback(pf2 | w, opt) 138 | 139 | val () = if ret > 0 && the_errno_get() = 0 then loop(e, w) else { 140 | val () = update_watcher(e, w, EPOLLIN lor EPOLLET) 141 | } 142 | } 143 | val () = loop(e, w) 144 | } 145 | val _ = if isclose then { 146 | val () = unregister_watcher(e, fd) 147 | val _ = close0_exn(fd) 148 | } 149 | } 150 | 151 | fun accept_conn(e: !Epoll(Server), w: !Watcher(Server,Server), evs: uint): void = () where { 152 | val fd = watcher_get_fd(w) 153 | fun loop(e: !Epoll(Server), w: !Watcher(Server,Server)): void = { 154 | val conn = $extfcall(int, "accept", fd, 0, 0) 155 | val conn = g1ofg0 conn 156 | val () = if conn > 0 then () where { 157 | val _ = setnonblocking(conn) 158 | val c = make_conn(conn) 159 | val w2 = make_watcher1(conn, do_read, c, free_conn) 160 | val () = register_watcher(e, w2, EPOLLIN lor EPOLLET) 161 | val () = loop(e, w) 162 | } 163 | } 164 | val () = loop(e, w) 165 | } 166 | 167 | fn make_threads(server: !Server): void = { 168 | // val+@S(s) = server 169 | // val fd = s.server_fd 170 | // prval () = fold@server 171 | // val watcher = make_watcher2(fd, accept_conn, server) 172 | // val+@S(s) = server 173 | // val () = register_watcher(s.epoll, watcher, EPOLLIN lor EPOLLET) 174 | // val () = run(s.epoll) 175 | // val () = free_epoll(s.epoll) 176 | // prval () = fold@server 177 | } 178 | 179 | implement make_server(port) = sh where { 180 | val s = get_server(port) 181 | val () = assertloc(s > 0) 182 | val () = println!("Server running at http://localhost:", port) 183 | val server = S(_) 184 | val S(se) = server 185 | val () = se.threadCount := 1 186 | val () = se.threads := list_vt_nil() 187 | val () = se.router := $HT.hashtbl_make_nil(i2sz 10) 188 | val () = se.server_fd := s 189 | val () = se.enable_gzip := false 190 | // val () = se.thread_pool := $POOL.make_pool(10) 191 | // val () = $POOL.init_pool(se.thread_pool) 192 | prval () = fold@server 193 | val sh = shared_make(server) 194 | val () = add_route(sh, "", "NOTFOUND", lam (req,resp) = (set_status_code(resp,404);copy("

404 not found

"))) 195 | } 196 | 197 | implement gclear_ref(x) = $UNSAFE.cast2void(x) 198 | 199 | fn free_server_(server: server_): void = { 200 | val+~S(s) = server 201 | // val () = $POOL.stop_pool(s.thread_pool) 202 | val () = list_vt_free(s.threads) 203 | val () = $HT.hashtbl_free(s.router) where { 204 | implement $HT.hashtbl_free$clear(k, v) = { 205 | val () = free(k) 206 | val () = cloptr_free($UNSAFE.castvwtp0{cloptr(void)}(v)) 207 | } 208 | } 209 | } 210 | 211 | // handles the SIGPIPE signal so we don't crash when sending to a client that closed the connection 212 | fn ignore_sigpipe(e: !Epoll(Server)): void = { 213 | fn handle_signal(e: !Epoll(Server), w: !Watcher(Server, void), evs: uint): void = () where { 214 | // just ignore it 215 | } 216 | var s: sigset_t? 217 | val i = sigemptyset(s) 218 | val () = if i = 0 then { 219 | prval() = opt_unsome(s) 220 | val i = sigaddset(s, SIGPIPE) 221 | val _ = sigprocmask(SIG_BLOCK, s, 0) 222 | val fd = signalfd(~1, s, 0) 223 | } else { 224 | prval() = opt_unnone(s) 225 | } 226 | } 227 | 228 | fn free_server_opt(opt: Option_vt(Server)): void = { 229 | val () = case+ opt of 230 | | ~Some_vt(ref) => free_server(ref) 231 | | ~None_vt() => () 232 | } 233 | 234 | implement run_server(serve) = { 235 | val (pf | server) = shared_lock(serve) 236 | val+@S(s) = server 237 | val fd = s.server_fd 238 | val threadCount = s.threadCount 239 | prval () = fold@server 240 | val () = shared_unlock(pf | serve, server) 241 | fun loop(i: int, ls: &List_vt(tid) >> _, sh0: !Server): void = { 242 | val () = if i > 0 then { 243 | val server_ref = shared_ref(sh0) 244 | val tid = athread_create_cloptr_join_exn(llam() => { 245 | val ref2 = shared_ref(server_ref) 246 | val e = make_epoll1(server_ref) 247 | val () = ignore_sigpipe(e) 248 | val () = listen(fd) 249 | val watcher = make_watcher1(fd, accept_conn, ref2, free_server_opt) 250 | val () = register_watcher(e, watcher, EPOLLIN lor EPOLLET) 251 | val () = run(e) 252 | val s = free_epoll(e) 253 | val () = free_server_opt(s) 254 | }) 255 | val () = assertloc(list_vt_length(ls) >= 0) 256 | val () = ls := list_vt_cons(tid, ls) 257 | val () = loop(i-1, ls, sh0) 258 | } 259 | } 260 | var threads = list_vt_nil() 261 | val () = loop(threadCount - 1, threads, serve) 262 | val server_ref = shared_ref(serve) 263 | val e = make_epoll1(serve) 264 | // ignore broken pipes 265 | val () = ignore_sigpipe(e) 266 | val () = list_vt_foreach(threads) where { 267 | implement list_vt_foreach$fwork(t, e) = { 268 | val () = athread_join(t) 269 | } 270 | } 271 | val () = list_vt_free(threads) 272 | val () = if threadCount = 1 then { 273 | val () = listen(fd) 274 | val watcher = make_watcher1(fd, accept_conn, server_ref, free_server_opt) 275 | val () = register_watcher(e, watcher, EPOLLIN lor EPOLLET) 276 | val () = run(e) 277 | } else { 278 | val-~None_vt() = shared_unref(server_ref) 279 | } 280 | val-~Some_vt(s) = free_epoll(e) 281 | val () = serve := s 282 | } 283 | 284 | implement free_server(server) = { 285 | val opt = $effmask_all(shared_unref(server)) 286 | val () = case+ opt of 287 | | ~Some_vt(s) => free_server_(s) 288 | | ~None_vt() => () 289 | } 290 | 291 | implement set_thread_count(serve, cnt) = { 292 | val (pf | server) = shared_lock(serve) 293 | val+@S(s) = server 294 | val () = s.threadCount := cnt 295 | prval() = fold@(server) 296 | val () = shared_unlock(pf | serve, server) 297 | } 298 | 299 | implement enable_gzip(serve) = { 300 | val (pf | server) = shared_lock(serve) 301 | val+@S(s) = server 302 | val () = s.enable_gzip := true 303 | prval() = fold@(server) 304 | val () = shared_unlock(pf | serve, server) 305 | } 306 | 307 | implement add_route(serve, method, route, handler) = { 308 | val (pf | server) = shared_lock(serve) 309 | val+@S(s) = server 310 | val key = string0_append(method, route) 311 | val opt = $HT.hashtbl_takeout_opt(s.router, key) 312 | val () = case+ opt of 313 | | ~Some_vt(func) => { 314 | val () = cloptr_free($UNSAFE.castvwtp0{cloptr(void)}func) 315 | } 316 | | ~None_vt() => () 317 | val-~None_vt() = $HT.hashtbl_insert_opt(s.router, key, $UNSAFE.castvwtp0{ptr}(handler)) 318 | prval() = fold@(server) 319 | val () = shared_unlock(pf | serve, server) 320 | } 321 | 322 | implement get(serve, route, handler) = add_route(serve, "GET", route, handler) 323 | implement post(serve, route, handler) = add_route(serve, "POST", route, handler) 324 | implement put(serve, route, handler) = add_route(serve, "PUT", route, handler) 325 | implement head(serve, route, handler) = add_route(serve, "HEAD", route, handler) 326 | implement delete(serve, route, handler) = add_route(serve, "DELETE", route, handler) 327 | -------------------------------------------------------------------------------- /src/DATS/request.dats: -------------------------------------------------------------------------------- 1 | #include "share/atspre_define.hats" 2 | #include "share/atspre_staload.hats" 3 | staload "./../SATS/types.sats" 4 | staload "./../SATS/request.sats" 5 | staload _ = "./../DATS/headers.dats" 6 | staload H = "./../SATS/headers.sats" 7 | #define ATS_DYNLOADFLAG 0 8 | 9 | assume Req = req_ 10 | 11 | implement make_req(headers, path, meth, body) = res where { 12 | val res = R(@{ 13 | headers = headers, 14 | path = path, 15 | method = meth, 16 | body = body 17 | }) 18 | } 19 | 20 | implement make_empty_req() = res where { 21 | val res = R(@{ 22 | headers = $H.new_headers(), 23 | path = copy(""), 24 | method = GET, 25 | body = copy("") 26 | }) 27 | } 28 | 29 | implement add_header_value(req, key, value) = { 30 | val+@R(r) = req 31 | val () = $H.put_header_value(r.headers, key, value) 32 | prval() = fold@req 33 | } 34 | 35 | implement set_path(req, path) = { 36 | val+@R(r) = req 37 | val () = free(r.path) 38 | val () = r.path := path 39 | prval() = fold@req 40 | } 41 | 42 | implement set_method(req, method) = { 43 | val+@R(r) = req 44 | val () = r.method := method 45 | prval() = fold@req 46 | } 47 | 48 | implement get_method(req) = res where { 49 | val+@R(r) = req 50 | val res = r.method 51 | prval() = fold@req 52 | } 53 | 54 | implement get_path(req) = res where { 55 | val+@R(r) = req 56 | val res = $UNSAFE.castvwtp1{string}(r.path) 57 | prval() = fold@req 58 | } 59 | 60 | implement set_body(req, body) = { 61 | val+@R(r) = req 62 | val () = free(r.body) 63 | val () = r.body := body 64 | prval() = fold@req 65 | } 66 | 67 | implement get_header_value(req, key) = res where { 68 | val+@R(r) = req 69 | val res = $H.get_header_value(r.headers, key) 70 | prval() = fold@req 71 | } 72 | 73 | implement get_body(req) = res where { 74 | val+@R(r) = req 75 | val res = $UNSAFE.castvwtp1{string}(r.body) 76 | val res = (if res = "" then None_vt() else Some_vt(res)): Option_vt(string) 77 | prval() = fold@req 78 | } 79 | 80 | implement print_request(req) = { 81 | val+@R(r) = req 82 | val () = case+ r.method of 83 | | GET() => println!("Method: GET") 84 | | POST() => println!("Method: POST") 85 | | HEAD() => println!("Method: HEAD") 86 | | PUT() => println!("Method: PUT") 87 | | DELETE() => println!("Method: DELETE") 88 | val () = println!("Path: ", r.path) 89 | overload print with $H.print_headers 90 | val () = println!(r.headers) 91 | prval() = fold@req 92 | } 93 | 94 | implement free_request(req) = { 95 | val+~R(r) = req 96 | val () = free(r.path) 97 | val () = $H.free_headers(r.headers) 98 | val () = free(r.body) 99 | } -------------------------------------------------------------------------------- /src/DATS/response.dats: -------------------------------------------------------------------------------- 1 | #include "share/atspre_define.hats" 2 | #include "share/atspre_staload.hats" 3 | staload "./../SATS/types.sats" 4 | staload "./../SATS/response.sats" 5 | staload _ = "./../DATS/headers.dats" 6 | staload H = "./../SATS/headers.sats" 7 | #define ATS_DYNLOADFLAG 0 8 | 9 | assume Resp = resp_ 10 | 11 | implement make_response() = res where { 12 | val res = RE(@{ 13 | headers = $H.new_headers(), 14 | status = 200, 15 | content_type = "text/html", 16 | body = copy("") 17 | }) 18 | } 19 | 20 | implement set_body(resp, body) = { 21 | val+@RE(r) = resp 22 | val () = free(r.body) 23 | val () = r.body := body 24 | prval() = fold@resp 25 | } 26 | 27 | implement get_status_code(resp) = res where { 28 | val+@RE(r) = resp 29 | val res = r.status 30 | prval() = fold@resp 31 | } 32 | 33 | implement set_status_code(resp, code) = { 34 | val+@RE(r) = resp 35 | val () = r.status := code 36 | prval() = fold@resp 37 | } 38 | 39 | implement get_content_type(resp) = res where { 40 | val+@RE(r) = resp 41 | val res = r.content_type 42 | prval() = fold@resp 43 | } 44 | 45 | implement set_content_type(resp, rt) = { 46 | val+@RE(r) = resp 47 | val () = r.content_type := rt 48 | prval() = fold@resp 49 | } 50 | 51 | implement free_response(resp) = { 52 | val+~RE(r) = resp 53 | val () = free(r.body) 54 | val () = $H.free_headers(r.headers) 55 | } -------------------------------------------------------------------------------- /src/HATS/includes.hats: -------------------------------------------------------------------------------- 1 | #include "share/atspre_define.hats" 2 | #include "share/atspre_staload.hats" 3 | staload "libats/libc/SATS/sys/socket.sats" 4 | staload "libats/libc/SATS/errno.sats" 5 | staload _ = "libats/libc/DATS/sys/socket.dats" 6 | staload "libats/libc/SATS/time.sats" 7 | // 8 | staload "libats/libc/SATS/unistd.sats" 9 | staload "libats/libc/SATS/sys/socket.sats" 10 | staload "libats/libc/SATS/sys/socket_in.sats" 11 | // 12 | staload "libats/libc/SATS/arpa/inet.sats" 13 | staload "libats/libc/SATS/netinet/in.sats" 14 | 15 | staload "libats/SATS/stringbuf.sats" 16 | staload _ = "libats/DATS/stringbuf.dats" 17 | 18 | #include "ats-pthread-ext.hats" 19 | #include "ats-epoll.hats" 20 | #include "ats-shared-vt.hats" 21 | #include "hashtable_vt.hats" 22 | #include "ats-threadpool.hats" 23 | #include "ats-libz.hats" 24 | #include "ats-shared-vt.hats" 25 | 26 | staload $EPOLL 27 | 28 | %{# 29 | #include "zlib.h" 30 | %} 31 | 32 | staload "./../SATS/http.sats" 33 | -------------------------------------------------------------------------------- /src/SATS/connection.sats: -------------------------------------------------------------------------------- 1 | #define ATS_PACKNAME "ats-http" 2 | #include "share/atspre_define.hats" 3 | #include "share/atspre_staload.hats" 4 | staload "./../SATS/types.sats" 5 | staload "./../SATS/http.sats" 6 | staload "libats/SATS/stringbuf.sats" 7 | 8 | datavtype conn_ = C of @{ 9 | fd= int, 10 | req=stringbuf, 11 | res=stringbuf, 12 | status=int, 13 | request=Req, 14 | response=Resp 15 | } 16 | 17 | fn make_conn(fd: int): Conn 18 | fn free_conn(conn: Option_vt(Conn)): void 19 | fn parse_conn_from_buffer{n:nat | n > 1}(conn: !Conn, buf: &(@[byte][n]), sz: int(n)): void 20 | fn parse_conn(conn: !Conn): void 21 | fn append_data{n, x:nat | n > 1 && x <= n}(conn: !Conn, buf: &(@[byte][n]), sz: int(n), cnt: int(x)): void 22 | fn method_to_string(m: Method): string 23 | fn set_status(conn: !Conn, status: int): void 24 | fn set_response(conn: !Conn, body: strptr): void 25 | fn clear_request_buffer(conn: !Conn): void 26 | fn clear_response_buffer(conn: !Conn): void 27 | fn call_handler(conn: !Conn, handler: Handler): strptr 28 | fn get_routing_key(conn: !Conn): strptr 29 | fn create_response(conn: !Conn, content: strptr): void 30 | fn create_response_gzip(conn: !Conn, content: strptr): void 31 | fn get_buffer(conn: !Conn, s: &size_t? >> size_t(n)): #[l:addr;n:nat] (bytes_v(l,n), bytes_v(l, n) - void | ptr(l)) 32 | -------------------------------------------------------------------------------- /src/SATS/headers.sats: -------------------------------------------------------------------------------- 1 | #define ATS_PACKNAME "ats-http-headers" 2 | #include "share/atspre_define.hats" 3 | #include "share/atspre_staload.hats" 4 | staload "./../SATS/types.sats" 5 | staload "./../SATS/http.sats" 6 | 7 | datavtype headers_ = H of @{ 8 | map=$HT.hashtbl(strptr,strptr) 9 | } 10 | 11 | fn parse_headers{n:int}(buf: &(@[byte][n])): Headers 12 | fn new_headers(): Headers 13 | fn get_header_value(h: !Headers, header: string): Option_vt(string) 14 | fn free_headers(h: Headers): void 15 | fn put_header_value(h: !Headers, header: strptr, value: strptr): void 16 | fn print_headers(h: !Headers): void 17 | 18 | overload print with print_headers 19 | -------------------------------------------------------------------------------- /src/SATS/http.sats: -------------------------------------------------------------------------------- 1 | #include "share/atspre_define.hats" 2 | #include "share/atspre_staload.hats" 3 | #include "ats-epoll.hats" 4 | #include "hashtable_vt.hats" 5 | #include "ats-threadpool.hats" 6 | staload "./../SATS/types.sats" 7 | #define ATS_PACKNAME "ats-http-http" 8 | 9 | fn http_write_err 10 | {n:nat}{l:addr | l > null} 11 | ( 12 | pf: !bytes_v(l,n) | fd: int, buf: ptr(l), ntotal: size_t(n) 13 | ): ssizeBtw(~1, n) = "ext#" 14 | 15 | fn http_read_err 16 | {n:nat}{l:addr | l > null} 17 | ( 18 | pf: !bytes_v(l, n) | fd: int, s: ptr(l), size_t(n) 19 | ): [m:int | m <= n] int(m) = "ext#" 20 | 21 | fn reuseport(fd: int): void = "ext#" 22 | 23 | vtypedef Handler = (!Req,!Resp) - strptr 24 | 25 | datavtype server_ = S of @{ 26 | router= $HT.hashtbl(strptr, ptr), 27 | threadCount= [n:nat] int(n), 28 | threads= List0_vt(lint), 29 | server_fd= [n:int | n > 0] int(n), 30 | enable_gzip=bool 31 | // thread_pool=$POOL.Pool 32 | } 33 | 34 | fn make_server(port: int): Server 35 | fn set_thread_count{n:nat}(server: !Server, threads: int(n)): void 36 | fn enable_gzip(server: !Server): void 37 | fn route(server: !Server, path: string): void 38 | fn run_server(server: &Server): void 39 | fn free_server(server: Server): void 40 | // fn free_server_(server: Option_vt(server_)): void 41 | fn add_route(server: !Server, method: string, route: string, handler: Handler): void 42 | fn get(server: !Server, route: string, handler: Handler): void 43 | fn post(server: !Server, route: string, handler: Handler): void 44 | fn put(server: !Server, route: string, handler: Handler): void 45 | fn delete(server: !Server, route: string, handler: Handler): void 46 | fn head(server: !Server, route: string, handler: Handler): void 47 | -------------------------------------------------------------------------------- /src/SATS/request.sats: -------------------------------------------------------------------------------- 1 | #define ATS_PACKNAME "ats-http" 2 | #include "share/atspre_define.hats" 3 | #include "share/atspre_staload.hats" 4 | staload "./../SATS/types.sats" 5 | 6 | datavtype req_ = R of @{ 7 | headers=Headers, 8 | path=strptr, 9 | method=Method, 10 | body=strptr 11 | } 12 | 13 | fn make_req(header: Headers, path: strptr, meth: Method, body: strptr): Req 14 | fn make_empty_req(): Req 15 | fn add_header_value(req: !Req, key: strptr, value: strptr): void 16 | fn set_path(req: !Req, path: strptr): void 17 | fn set_method(req: !Req, method: Method): void 18 | fn get_method(req: !Req): Method 19 | fn get_path(req: !Req): string 20 | fn set_body(req: !Req, body: strptr): void 21 | fn get_header_value(req: !Req, key: string): Option_vt(string) 22 | fn get_body(req: !Req): Option_vt(string) 23 | fn print_request(req: !Req): void 24 | fn free_request(req: Req): void 25 | 26 | overload print with print_request 27 | -------------------------------------------------------------------------------- /src/SATS/response.sats: -------------------------------------------------------------------------------- 1 | #define ATS_PACKNAME "ats-http-response" 2 | #include "share/atspre_define.hats" 3 | #include "share/atspre_staload.hats" 4 | staload "./../SATS/types.sats" 5 | 6 | datavtype resp_ = RE of @{ 7 | headers=Headers, 8 | status=int, 9 | content_type=string, 10 | body=strptr 11 | } 12 | 13 | fn make_response(): Resp 14 | fn set_body(req: !Resp, body: strptr): void 15 | fn free_response(req: Resp): void 16 | fn print_request(req: !Resp): void 17 | fn set_status_code(res: !Resp, code: int): void 18 | fn get_status_code(res: !Resp): int 19 | fn set_content_type(res: !Resp, code: string): void 20 | fn get_content_type(res: !Resp): string 21 | 22 | overload print with print_request 23 | -------------------------------------------------------------------------------- /src/SATS/types.sats: -------------------------------------------------------------------------------- 1 | #define ATS_PACKNAME "ats-http" 2 | #include "share/atspre_define.hats" 3 | #include "share/atspre_staload.hats" 4 | 5 | absvtype Server 6 | absvtype Req 7 | absvtype Headers 8 | absvtype Conn 9 | absvtype Resp 10 | 11 | datatype Method = 12 | | GET of () 13 | | PUT of () 14 | | POST of () 15 | | HEAD of () 16 | | DELETE of () 17 | -------------------------------------------------------------------------------- /src/ats-http.hats: -------------------------------------------------------------------------------- 1 | #include "./HATS/includes.hats" 2 | staload _ = "./DATS/http.dats" 3 | staload _ = "./DATS/headers.dats" 4 | staload _ = "./DATS/request.dats" 5 | staload _ = "./DATS/response.dats" 6 | staload _ = "./DATS/connection.dats" 7 | staload HTTP = "./SATS/http.sats" 8 | staload REQ = "./SATS/request.sats" 9 | staload RESP = "./SATS/response.sats" 10 | 11 | %{# 12 | #include "CATS/ats-http.cats" 13 | %} 14 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | ATSCC=$(PATSHOME)/bin/patscc 2 | ATSOPT=$(PATSHOME)/bin/patsopt 3 | 4 | ATSFLAGS+= 5 | 6 | CFLAGS+=-DATS_MEMALLOC_LIBC -D_DEFAULT_SOURCE -I $(PATSHOME)/ccomp/runtime -I $(PATSHOME) -O3 -I src $(INCLUDES) 7 | LDFLAGS+=-L $(PATSHOME)/ccomp/atslib/lib 8 | LIBS+=-latslib -ldl -lpthread 9 | 10 | NAME := tests 11 | SRCDIR := 12 | vpath %.dats src 13 | vpath %.dats src/DATS 14 | vpath %.sats src/SATS 15 | SRCS := $(shell find $(SRCDIR) -name '*.dats' -type f -exec basename {} \;) 16 | DDIR := .build 17 | OBJ := $(patsubst %.dats,$(DDIR)/%.o,$(SRCS)) 18 | 19 | .PHONY: all clean fclean re 20 | 21 | all: $(NAME) 22 | 23 | $(NAME): $(OBJ) 24 | $(CC) $(LDFLAGS) $^ $(LIBS) -o $@ 25 | 26 | $(DDIR)/%.o: %.c | $(DDIR) 27 | $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $< 28 | 29 | %.c: %.dats 30 | $(ATSOPT) $(ATSFLAGS) -o $(@F) -d $< 31 | 32 | $(SDIR) $(DDIR): 33 | @mkdir $@ 34 | 35 | clean: 36 | $(RM) -r $(SDIR) $(DDIR) 37 | 38 | fclean: clean 39 | $(RM) $(NAME) 40 | 41 | re: fclean all 42 | 43 | -------------------------------------------------------------------------------- /tests/conanfile.py: -------------------------------------------------------------------------------- 1 | from atsconan import ATSConan 2 | 3 | class ATSConan(ATSConan): 4 | name = "ats-http-tests" 5 | version = "0.1" 6 | settings = "os", "compiler", "build_type", "arch" 7 | exports_sources = "*" 8 | options = {"shared": [True, False], "fPIC": [True, False]} 9 | default_options = {"shared": False, "fPIC": True} 10 | requires = [ 11 | "ats-http/0.2@randy.valis/testing", 12 | "ats-sqlite3/0.1@randy.valis/testing", 13 | "ats-json/0.1@randy.valis/testing" 14 | ] 15 | build_requires = "ats-unit-testing/0.1@randy.valis/testing" 16 | 17 | def package(self): 18 | self.copy("tests", dst="target", keep_path=False) 19 | 20 | def deploy(self): 21 | self.copy("*", src="target", dst="bin") 22 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello 7 | 8 | 9 |

Hello World

10 |

ATS-HTTP

11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xran-deex/ats-http/11e0089b2d0c34f7901d494edd24a535e5bf6500/tests/test.db -------------------------------------------------------------------------------- /tests/tests.dats: -------------------------------------------------------------------------------- 1 | #include "ats-http.hats" 2 | staload "libats/libc/SATS/string.sats" 3 | #include "ats-sqlite3.hats" 4 | #include "ats-json.hats" 5 | 6 | staload $HTTP 7 | staload $REQ 8 | staload $RESP 9 | staload $SQLITE 10 | staload $JSON 11 | staload $JSON_TYPES 12 | 13 | implement main(argc, argv) = 0 where { 14 | var server = make_server(8888) 15 | val () = set_thread_count(server, 6) 16 | // val () = enable_gzip(server) 17 | 18 | val () = get(server, "/", lam (req,resp) = res where { 19 | val in_opt = fileref_open_opt("index.html", file_mode_r) 20 | val () = set_content_type(resp, "text/html") 21 | val res = (case+ in_opt of 22 | | ~Some_vt(inp) => res where { 23 | val () = set_status_code(resp, 200) 24 | val res = fileref_get_file_string(inp) 25 | val _ = fileref_close(inp) 26 | } 27 | | ~None_vt () => res where { 28 | val () = set_status_code(resp, 404) 29 | val () = println!("file not found") 30 | val res = copy("not found") 31 | }): strptr 32 | }) 33 | 34 | val () = get(server, "/hello", lam (req,resp) = copy("Hello World") where { 35 | val () = set_status_code(resp, 200) 36 | val () = set_content_type(resp, "text/plain") 37 | }) 38 | 39 | val () = get(server, "/json", lam (req,resp) = res where { 40 | val () = set_status_code(resp, 200) 41 | val () = set_content_type(resp, "application/json") 42 | val obj = new_object() 43 | val () = add_value_to_object_exn(obj, "hello", String(copy "world")) 44 | val res = json_to_string(obj) 45 | val () = free_json(obj) 46 | }) 47 | 48 | val () = post(server, "/json", lam (req,resp) = copy("") where { 49 | val () = set_status_code(resp, 200) 50 | val () = set_content_type(resp, "application/json") 51 | val body = get_body(req) 52 | val () = case+ body of 53 | | ~Some_vt(b) => { 54 | val () = println!("Body: ", b) 55 | val-~$RESULT.Ok(json) = parse_json_string(b) 56 | val-~Some_vt(str) = get_string(json, "hello") 57 | val () = println!("VALUE: ", str) 58 | val () = free(str) 59 | val () = free_json(json) 60 | } 61 | | ~None_vt() => () 62 | }) 63 | 64 | val () = get(server, "/database", lam (req, resp) = res where { 65 | val () = set_status_code(resp, 200) 66 | val () = set_content_type(resp, "text/plain") 67 | var db: sqlite3_ptr0? 68 | val res = open("test.db", db) 69 | // yes, what follows is ugly, but hey, it works 70 | val str = (if res = SQLITE_OK then retu where { 71 | var stmt: sqlite3_stmt0? 72 | val sql = "select * from Thing where age > ?1 and name = ?2" 73 | val res = prepare(db, sql, sz2i(length(sql)), stmt, the_null_ptr) 74 | val retu = (if res = SQLITE_OK then retu where { 75 | val _ = bind_int(stmt, 1, 20) 76 | val _ = bind_text(stmt, 2, "Joe", ~1, the_null_ptr) 77 | val ret = step(stmt) 78 | val retu = (if ret = SQLITE_ROW then ret where { 79 | val sb = stringbuf_make_nil_int(100) 80 | val ret = constchar2string(column_text(stmt, 0)) 81 | val _ = stringbuf_insert_string(sb, ret) 82 | val _ = stringbuf_insert_string(sb, " is ") 83 | val ret = constchar2string(column_text(stmt, 1)) 84 | val _ = stringbuf_insert_string(sb, ret) 85 | val _ = stringbuf_insert_string(sb, " years old.") 86 | var size: size_t? 87 | val ret = stringbuf_getfree_strnptr(sb, size) 88 | val _ = finalize(stmt) 89 | } else ret where { 90 | val _ = finalize(stmt) 91 | val ret = string1_copy "" 92 | }): [n:int] strnptr(n) 93 | } else ret where { 94 | val _ = finalize(stmt) 95 | val ret = string1_copy "" 96 | }): [n:int] strnptr(n) 97 | val _ = $SQLITE.close(db) 98 | } else ret where { 99 | val _ = $SQLITE.close(db) 100 | val ret = string1_copy "" 101 | }): [n:int] strnptr(n) 102 | val res = strnptr2strptr str 103 | }) 104 | 105 | val () = post(server, "/goodbye", lam (req,resp) = res where { 106 | val () = println!(req) 107 | val h = get_header_value(req, "Host") 108 | val () = case+ h of 109 | | ~Some_vt(header) => println!("Host: ", header) 110 | | ~None_vt() => () 111 | val body = get_body(req) 112 | val () = case+ body of 113 | | ~Some_vt(b) => println!("Body: ", b) 114 | | ~None_vt() => () 115 | val () = set_status_code(resp, 200) 116 | val res = copy("Goodbye world") 117 | }) 118 | val () = run_server(server) 119 | val () = free_server(server) 120 | } 121 | 122 | --------------------------------------------------------------------------------