diff options
Diffstat (limited to 'lib/http')
-rw-r--r-- | lib/http/bld.sub | 34 | ||||
-rw-r--r-- | lib/http/client.myr | 276 | ||||
-rw-r--r-- | lib/http/h.myr | 90 | ||||
-rw-r--r-- | lib/http/parse.myr | 278 | ||||
-rw-r--r-- | lib/http/server.myr | 100 | ||||
-rw-r--r-- | lib/http/session.myr | 80 | ||||
-rw-r--r-- | lib/http/srvdot.myr | 66 | ||||
-rw-r--r-- | lib/http/status.myr | 67 | ||||
-rw-r--r-- | lib/http/types.myr | 83 | ||||
-rw-r--r-- | lib/http/url.myr | 238 |
10 files changed, 1312 insertions, 0 deletions
diff --git a/lib/http/bld.sub b/lib/http/bld.sub new file mode 100644 index 0000000..0c4834b --- /dev/null +++ b/lib/http/bld.sub @@ -0,0 +1,34 @@ +bin h {noinst}= + h.myr + + lib http + lib ../sys:sys + lib ../std:std + lib ../bio:bio + lib ../thread:thread +;; + +bin srvdot {noinst}= + srvdot.myr + + lib http + lib ../sys:sys + lib ../std:std + lib ../bio:bio + lib ../thread:thread +;; + +lib http = + parse.myr + types.myr + url.myr + client.myr + server.myr + session.myr + status.myr + + lib ../sys:sys + lib ../std:std + lib ../bio:bio + lib ../thread:thread +;; diff --git a/lib/http/client.myr b/lib/http/client.myr new file mode 100644 index 0000000..770c233 --- /dev/null +++ b/lib/http/client.myr @@ -0,0 +1,276 @@ +use std +use bio + +use "types" +use "session" +use "parse" + +pkg http = + /* simple versions */ + const get : (s : session#, r : url# -> std.result(resp#, err)) + const head : (s : session#, r : url# -> std.result(resp#, err)) + const put : (s : session#, r : url#, data : byte[:] -> std.result(resp#, err)) + const post : (s : session#, r : url#, data : byte[:] -> std.result(resp#, err)) + const delete : (s : session#, r : url# -> std.result(resp#, err)) + const options : (s : session#, r : url# -> std.result(resp#, err)) + const trace : (s : session#, r : url# -> std.result(resp#, err)) + + /* request based versions */ + const getreq : (s : session#, r : req# -> std.result(resp#, err)) + const headreq : (s : session#, r : req# -> std.result(resp#, err)) + const putreq : (s : session#, r : req#, data : byte[:] -> std.result(resp#, err)) + const postreq : (s : session#, r : req#, data : byte[:] -> std.result(resp#, err)) + const deletereq : (s : session#, r : req# -> std.result(resp#, err)) + const optionsreq : (s : session#, r : req# -> std.result(resp#, err)) + const tracereq : (s : session#, r : req# -> std.result(resp#, err)) + + const freeresp : (r : resp# -> void) +;; + +const get = {s, path; -> getreq(s, &[.url=path])} +const head = {s, path; -> headreq(s, &[.url=path])} +const put = {s, path, data; -> putreq(s, &[.url=path], data)} +const post = {s, path, data; -> postreq(s, &[.url=path], data)} +const delete = {s, path; -> deletereq(s, &[.url=path])} +const options = {s, path; -> optionsreq(s, &[.url=path])} +const trace = {s, path; -> tracereq(s, &[.url=path])} + + +const getreq = {s, r + match request(s, `Get, r, `std.None) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const headreq = {s, r + match request(s, `Head, r, `std.None) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, false) +} + +const putreq = {s, r, data + match request(s, `Put, r, `std.Some data) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const postreq = {s, r, data + match request(s, `Post, r, `std.Some data) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const deletereq = {s, r + match request(s, `Delete, r, `std.None) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const optionsreq = {s, r + match request(s, `Options, r, `std.None) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const tracereq = {s, r + match request(s, `Trace, r, `std.None) + | `std.Ok _: /* nothing */ + | `std.Err e: -> `std.Err e + ;; + + -> response(s, true) +} + +const response = {s, body + var resp + + resp = std.mk([ + .hdrs = [][:], + .len = 0, + .err = `std.None, + .reason = "", + .status = 0, + .enc = `Length, + ]) + + if parseresp(s, resp) + if !body + -> `std.Ok resp + else + match readbody(s, resp) + | `std.Ok buf: resp.body = buf + | `std.Err e: -> `std.Err e + ;; + ;; + else + match resp.err + | `std.Some e: -> `std.Err e + | `std.None: -> `std.Err `Ewat + ;; + ;; + -> `std.Ok resp +} + +const request = {s, method, r, data + /* status */ + ioput(s, "{} {p} HTTP/1.1\r\n", method, r.url) + + /* headers */ + ioput(s, "Host: {}\r\n", s.host) + ioput(s, "User-Agent: {}\r\n", s.ua) + match data + | `std.Some d: ioput(s, "Content-Length: {}\r\n", d.len) + | `std.None: /* nothing to do */ + ;; + for (k, v) : r.hdrs + ioput(s, "{}: {}\r\n", k, v) + ;; + ioput(s, "\r\n") + + /* body */ + match data + | `std.None: /* nothing to do */ + | `std.Some d: + iowrite(s, d) + ioput(s, "\r\n") + ;; + ioflush(s) + + if s.err + -> `std.Err `Econn + else + -> `std.Ok void + ;; + +} + +const readbody = {s, r -> std.result(byte[:], err) + match r.enc + | `Length: -> readlenbody(s, r) + | `Chunked: -> readchunkedbody(s, r) + | badenc: std.fatal("unsupported encoding {}\n", badenc) + ;; +} + +const readlenbody = {s, r + var buf + + buf = "" + if r.len == 0 + -> `std.Ok buf + ;; + + buf = std.slalloc(r.len) + match bio.read(s.f, buf) + | `std.Err e: goto shortread + | `std.Ok rd: + if rd.len != r.len + goto shortread + ;; + ;; + -> `std.Ok buf +:shortread + std.slfree(buf) + -> `std.Err `Eshort +} + +const __init__ = { + var m + + m = `Get + std.fmtinstall(std.typeof(m), fmtmethod, [][:]) +} + +const readchunkedbody = {s, r + var buf, len + + buf = "" + len = 0 + while true + match parsechunksz(s) + | `std.Err e: + std.slfree(buf) + -> `std.Err e + | `std.Ok 0: + break + | `std.Ok sz: + std.slgrow(&buf, buf.len + sz) + match bio.read(s.f, buf[len:len + sz]) + | `std.Err e: + std.slfree(buf) + -> `std.Err `Econn + | `std.Ok str: + if str.len != sz + std.slfree(buf) + -> `std.Err `Eshort + ;; + len += sz + match checkendln(s) + | `std.Ok _: /* nothing */ + | `std.Err e: + std.slfree(buf) + -> `std.Err e + ;; + ;; + ;; + ;; + -> `std.Ok buf +} + +const checkendln = {s + var r + + match bio.readln(s.f) + | `std.Err e: r = `std.Err `Econn + | `std.Ok crlf: + if std.strstrip(crlf).len == 0 + r = `std.Ok void + else + r = `std.Err `Eproto + ;; + std.slfree(crlf) + ;; + -> r +} + +const fmtmethod = {sb, ap, opt + var r + + r = std.vanext(ap) + match r + | `Get: std.sbputs(sb, "GET") + | `Head: std.sbputs(sb, "HEAD") + | `Put: std.sbputs(sb, "PUT") + | `Post: std.sbputs(sb, "POST") + | `Delete: std.sbputs(sb, "DELETE") + | `Trace: std.sbputs(sb, "TRACE") + | `Options: std.sbputs(sb, "OPTIONS") + ;; +} + +const freeresp = {r + for (k, v) : r.hdrs + std.slfree(k) + std.slfree(v) + ;; + std.slfree(r.reason) + std.free(r) +} diff --git a/lib/http/h.myr b/lib/http/h.myr new file mode 100644 index 0000000..9a00e2e --- /dev/null +++ b/lib/http/h.myr @@ -0,0 +1,90 @@ +use std +use http + +const main = {args + var data, method, showhdr, hdrs, out + var s, u, r + var cmd + + cmd = std.optparse(args, &[ + .argdesc = "url...", + .minargs = 1, + .opts = [ + [.opt='m', .arg="method", .desc="http method to use"], + [.opt='d', .arg="data", .desc="data to put in request body"], + [.opt='o', .arg="out", .desc="output file name"], + [.opt='H', .desc="show headers"], + [.opt='D', .arg="hdr", .desc="define custom header"] + ][:] + ]) + + showhdr = false + method = "get" + data = "" + hdrs = [][:] + out = "" + for opt : cmd.opts + match opt + | ('m', m): method = m + | ('d', d): data = d + | ('o', o): out = o + | ('H', ""): showhdr = true + | ('D', def): parsedef(&hdrs, def) + | _: std.die("unreachable") + ;; + ;; + + for url : cmd.args + if !std.hasprefix(url, "http") + url = std.fmt("http://{}", url) + ;; + u = std.try(http.parseurl(url)) + s = std.try(http.mksession(u.schema, u.host, u.port)) + + match method + | "get": r = http.getreq(s, &[.url=u, .hdrs=hdrs]) + | "head": r = http.headreq(s, &[.url=u, .hdrs=hdrs]) + | "delete": r = http.deletereq(s, &[.url=u, .hdrs=hdrs]) + | "trace": r = http.tracereq(s, &[.url=u, .hdrs=hdrs]) + | "options": r = http.optionsreq(s, &[.url=u, .hdrs=hdrs]) + | "put": r = http.putreq(s, &[.url=u, .hdrs=hdrs], data) + | "post": r = http.postreq(s, &[.url=u, .hdrs=hdrs], data) + | unknown: std.fatal("unknown method '{}'\n", unknown) + ;; + + match r + | `std.Ok resp: + if showhdr + std.put("status: {}\n", resp.status) + for (k, v) : resp.hdrs + std.put("{}: {}\n", k, v) + ;; + ;; + if out.len != 0 + if !std.blat(out, resp.body, 0o644) + std.fatal("could not write output: {}\n", out) + ;; + else + std.fblat(std.Out, resp.body) + ;; + http.freeresp(resp) + | `std.Err e: + std.put("{}\n", e) + ;; + http.urlfree(u) + ;; +} + +const parsedef = {hdrs, hdr + var key, val + + match std.strfind(hdr, ":") + | `std.None: + std.fatal("bad header string {}\n", hdr) + | `std.Some idx: + key = std.sldup(std.strstrip(hdr[:idx])) + val = std.sldup(std.strstrip(hdr[idx+1:])) + std.slpush(hdrs, (key, val)) + ;; +} + diff --git a/lib/http/parse.myr b/lib/http/parse.myr new file mode 100644 index 0000000..fabf7a2 --- /dev/null +++ b/lib/http/parse.myr @@ -0,0 +1,278 @@ +use std +use bio + +use "types" + +pkg http = + pkglocal const parsereq : (s : session# -> std.result(req#, err)) + pkglocal const parseresp : (s : session#, r : resp# -> bool) + pkglocal const parsechunksz : (s : session# -> std.result(std.size, err)) + pkglocal const parsenumber : (str : byte[:]#, base : int -> std.option(int)) +;; + +const parsereq = {s + var r, err + + r.hdrs = [][:] + r.url = std.mk([ + .schema=`Http, + .host=std.sldup(s.host), + .port=s.port, + .path="", + .params=[][:], + ]) + + match bio.readln(s.f) + | `std.Err e: + err = `Econn + goto error + | `std.Ok ln: + match parsereqstatus(s, r, ln) + | `std.Ok void: + | `std.Err e: + std.slfree(ln) + err = e + -> `std.Err e + ;; + std.slfree(ln) + ;; + + while true + match bio.readln(s.f) + | `std.Err e: + err = `Econn + goto error + | `std.Ok ln: + if std.strstrip(ln).len == 0 + std.slfree(ln) + break + ;; + match parsehdr(s, ln) + | `std.Ok kvp: + std.slpush(&r.hdrs, kvp) + | `std.Err e: + std.slfree(ln) + -> `std.Err e + ;; + std.slfree(ln) + ;; + ;; + -> `std.Ok std.mk(r) +:error + -> `std.Err err +} + +const parseresp = {s, r : resp# + match bio.readln(s.f) + | `std.Err _: r.err = `std.Some `Econn + | `std.Ok ln: + if !parserespstatus(s, r, ln) + std.slfree(ln) + -> false + ;; + std.slfree(ln) + ;; + + while true + match bio.readln(s.f) + | `std.Err e: r.err = `std.Some `Econn + | `std.Ok ln: + if std.strstrip(ln).len == 0 + std.slfree(ln) + break + ;; + match parsehdr(s, ln) + | `std.Ok kvp: + std.slpush(&r.hdrs, kvp) + | `std.Err e: + r.err = `std.Some e + std.slfree(ln) + -> false + ;; + std.slfree(ln) + ;; + ;; + + match getenc(r) + | `std.Ok `Length: + r.enc = `Length + match getlen(r) + | `std.Some n: + r.len = n + | `std.None: + r.err = `std.Some `Eproto + -> false + ;; + | `std.Ok enc: + r.enc = enc + | `std.Err e: + r.err = `std.Some e + -> false + ;; + -> true + +} + +const parsereqstatus = {s, r, ln + match parseword(&ln) + | `std.Some "GET": r.method = `Get + | `std.Some "HEAD": r.method = `Head + | `std.Some "POST": r.method = `Post + | `std.Some "DELETE": r.method = `Delete + | `std.Some "TRACE": r.method = `Trace + | `std.Some "OPTIONS": r.method = `Options + | `std.Some _: -> `std.Err `Eproto + | `std.None: -> `std.Err `Eproto + ;; + + match parseword(&ln) + | `std.Some w: r.url.path = std.sldup(w) + | `std.None: -> `std.Err `Eproto + ;; + + match parseword(&ln) + | `std.Some "HTTP/1.1": /* ok */ + | `std.Some w: std.put("warn: http version '{}'\n", w) + | `std.None: -> `std.Err `Eproto + ;; + + ln = std.strfstrip(ln) + -> `std.Ok void +} + +const parserespstatus = {s, r, ln + /* HTTP/1.1 */ + ln = std.strfstrip(ln) + if !std.chomp(&ln, "HTTP") + r.err = `std.Some `Eproto + -> false + ;; + if !std.chomp(&ln, "/1.1") + r.err = `std.Some `Eproto + -> false + ;; + + ln = std.strfstrip(ln) + match parsenumber(&ln, 10) + | `std.Some n: + r.status = n + | `std.None: + r.err = `std.Some `Eproto + -> false + ;; + + ln = std.strfstrip(ln) + r.reason = std.sldup(ln) + -> true +} + +const parsehdr = {s, ln + var key, val + + match std.strfind(ln, ":") + | `std.Some idx: + key = std.sldup(std.strstrip(ln[:idx])) + val = std.sldup(std.strstrip(ln[idx+1:])) + -> `std.Ok (key, val) + | `std.None: + -> `std.Err `Ehdr + ;; +} + +const getlen = {r + match findhdr(r, "Content-Length") + | `std.Some v: + match std.intparsebase(v, 10) + | `std.Some n: -> `std.Some n + | `std.None: -> `std.None + ;; + | `std.None: + -> `std.None + ;; +} + +const parsechunksz = {s + var ret, str + + match bio.readln(s.f) + | `std.Err e: ret = `std.Err `Econn + | `std.Ok ln: + str = ln + match parsenumber(&str, 16) + | `std.Some n: ret = `std.Ok (n : std.size) + | `std.None: + ret = `std.Err `Eproto + ;; + std.slfree(ln) + ;; + -> ret +} + +const getenc = {r + match findhdr(r, "Transfer-Encoding") + | `std.None: -> `std.Ok `Length + | `std.Some "chunked": -> `std.Ok `Chunked + | `std.Some "compress": -> `std.Ok `Compress + | `std.Some "deflate": -> `std.Ok `Deflate + | `std.Some "gzip": -> `std.Ok `Gzip + | `std.Some unknown: -> `std.Err `Eenc + ;; +} + +const findhdr = {r, name + for (k, v) : r.hdrs + if std.strcaseeq(k, name) + -> `std.Some v + ;; + ;; + -> `std.None +} + +const parseword = {ln + var w, end + + ln# = std.strfstrip(ln#) + end = 0 + for var i = 0; i < ln#.len; i++ + if i == ln#.len - 1 + end = i + 1 + elif std.isspace(std.decode(ln#[i:])) + end = i + break + ;; + ;; + if end == 0 + -> `std.None + else + w = ln#[:end] + ln# = ln#[end:] + -> `std.Some w + ;; +} + +const parsenumber = {ln, base + var n, ok + var c, s, dig + + n = 0 + s = ln# + ok = false + while true + (c, s) = std.strstep(s) + dig = std.charval(c, base) + if dig >= 0 && dig < base + ok = true + n *= base + n += dig + else + break + ;; + ;; + ln# = s + if ok + -> `std.Some n + else + -> `std.None + ;; +} + diff --git a/lib/http/server.myr b/lib/http/server.myr new file mode 100644 index 0000000..0cfdcb0 --- /dev/null +++ b/lib/http/server.myr @@ -0,0 +1,100 @@ +use bio +use std +use thread + +use "types" +use "session" +use "parse" + +pkg http = + const announce : (ds : byte[:] -> std.result(server#, err)) + const shutdown : (srv : server# -> void) + const serve : (srv : server#, fn : (srv : server#, s : session#, req : req# -> void) -> void) + const respond : (srv : server#, sess : session#, resp : resp# -> void) +;; + +const announce = {ds + match std.announce(ds) + | `std.Err e: -> `std.Err `Econn + | `std.Ok a: + -> `std.Ok std.mk([ + .refs=1, + .ann=a, + .quit=false + ]) + ;; +} + +const serve = {srv, fn + while !srv.quit + match waitconn(srv) + | `std.Ok fd: + ref(srv) + thread.spawn({;communicate(srv, fd, fn)}) + | `std.Err e: /* eh? */ + ;; + ;; + unref(srv) +} + +const communicate = {srv, fd, fn + var s + + s = mksrvsession(fd) + while !srv.quit + match parsereq(s) + | `std.Ok req: fn(srv, s, req) + | `std.Err e: break + ;; + ;; + std.close(fd) + unref(srv) +} + +const respond = {srv, s, resp + var sb + + sb = std.mksb() + ioput(s, "HTTP/1.1 {} {}\r\n", resp.status, statusstr(resp.status)) + ioput(s, "Content-Length: {}\r\n", resp.body.len) + ioput(s, "Encoding: {}\r\n", resp.enc) + for (k, v) : resp.hdrs + ioput(s, "{}: {}\r\n", k, v) + ;; + ioput(s, "\r\n") + iowrite(s, resp.body) + ioflush(s) +} + +const statusstr = {st + match st + | 200: -> "OK" + | 404: -> "Not Found" + | 503: -> "Internal Error" + | _: -> "Bad State" + ;; +} + +const shutdown = {srv + std.aclose(srv.ann) + srv.quit = true +} + +const waitconn = {srv + match std.accept(srv.ann) + | `std.Ok fd: -> `std.Ok fd + | `std.Err e: -> `std.Err `Econn + ;; +} + + +const ref = {srv + thread.xadd(&srv.refs, 1) +} + +const unref = {srv + thread.xadd(&srv.refs, -1) + if thread.xget(&srv.refs) == 0 + std.free(srv) + ;; +} diff --git a/lib/http/session.myr b/lib/http/session.myr new file mode 100644 index 0000000..4091165 --- /dev/null +++ b/lib/http/session.myr @@ -0,0 +1,80 @@ +use std +use bio + +use "types" + +pkg http = + const mksession : (schema : schema, host : byte[:], port : uint16 -> std.result(session#, err)) + const mksrvsession : (fd : std.fd -> session#) + const freesession : (s : session# -> void) + + pkglocal const ioput : (s : session#, fmt : byte[:], args : ... -> bool) + pkglocal const iowrite : (s : session#, buf : byte[:] -> bool) + pkglocal const ioflush : (s : session# -> void) +;; + +const mksession = {schema, host, port + var s, sess + + match schema + | `Http: /* nothing */ + | `Https: std.fatal("unsupported protocol\n") + ;; + + s = std.fmt("tcp!{}!{}", host, port) + match std.dial(s) + | `std.Err e: sess = `std.Err `Econn + | `std.Ok fd: sess = `std.Ok std.mk([ + .err = false, + .ua = std.sldup("Myrfoo HTTP"), + .host = std.sldup(host), + .f = bio.mkfile(fd, bio.Rw) + ]) + ;; + std.slfree(s) + -> sess +} + +const mksrvsession = {fd + -> std.mk([ + .err = false, + .srvname = std.sldup("Myrfoo HTTP Server"), + .f = bio.mkfile(fd, bio.Rw), + ]) +} + +const freesession = {s + bio.close(s.f) + std.slfree(s.host) + std.slfree(s.ua) + std.free(s) +} + +const ioput = {s, fmt, args + var ap + + if s.err + -> false + ;; + ap = std.vastart(&args) + match bio.putv(s.f, fmt, &ap) + | `std.Ok _: /* nothing */ + | `std.Err _: s.err = true + ;; + -> s.err +} + +const iowrite = {s, buf + if s.err + -> false + ;; + match bio.write(s.f, buf) + | `std.Ok _: /* nothing */ + | `std.Err _: s.err = true + ;; + -> s.err +} + +const ioflush = {s + bio.flush(s.f) +} diff --git a/lib/http/srvdot.myr b/lib/http/srvdot.myr new file mode 100644 index 0000000..e6c0a21 --- /dev/null +++ b/lib/http/srvdot.myr @@ -0,0 +1,66 @@ +use std +use http + +const main = {args + var srv, ann, cmd + + cmd = std.optparse(args, &[ + .maxargs=0, + .opts = [[.opt='a', .arg="ann", .desc="announce on `ann`"]][:] + ]) + ann = "tcp!localhost!8080" + for opt : cmd.opts + match opt + | ('a', a): ann = a + | _: std.die("unreachable") + ;; + ;; + + match http.announce(ann) + | `std.Ok s: srv = s + | `std.Err e: std.fatal("unable to announce: {}\n", e) + ;; + + http.serve(srv, route) +} + +const route = {srv, sess, req + std.put("Reading path {}\n", req.url.path) + match req.url.path + | "/ping": respond(srv, sess, 200, "pong") + | "/quit": http.shutdown(srv) + | fspath: showfile(srv, sess, req.url.path) + ;; +} + +const showfile = {srv, sess, path + var eb : byte[128] + var p + + p = std.pathcat(".", path) + match std.slurp(p) + | `std.Ok buf: + respond(srv, sess, 200, buf) + std.slfree(buf) + | `std.Err e: + respond(srv, sess, 404, std.bfmt(eb[:], "error reading {}: {}\n", p, e)) + ;; + std.slfree(p) +} + + +const respond = {srv, sess, status, body + var resp + + resp = std.mk([ + .status=status, + .hdrs = [][:], + .len = 0, + .err = `std.None, + .reason = "", + .body = body, + .enc = `http.Length + ]) + http.respond(srv, sess, resp) + std.free(resp) +} diff --git a/lib/http/status.myr b/lib/http/status.myr new file mode 100644 index 0000000..df247be --- /dev/null +++ b/lib/http/status.myr @@ -0,0 +1,67 @@ +use "types" + +pkg http = + const Continue : status = 100 /* RFC 7231, 6.2.1*/ + const SwitchingProtocols : status = 101 /* RFC 7231, 6.2.2*/ + const Processing : status = 102 /* RFC 2518, 10.1*/ + + const Ok : status = 200 /* RFC 7231, 6.3.1*/ + const Created : status = 201 /* RFC 7231, 6.3.2*/ + const Accepted : status = 202 /* RFC 7231, 6.3.3*/ + const NonAuthoritativeInfo : status = 203 /* RFC 7231, 6.3.4*/ + const NoContent : status = 204 /* RFC 7231, 6.3.5*/ + const ResetContent : status = 205 /* RFC 7231, 6.3.6*/ + const PartialContent : status = 206 /* RFC 7233, 4.1*/ + const Multi : status = 207 /* RFC 4918, 11.1*/ + const AlreadyReported : status = 208 /* RFC 5842, 7.1*/ + const IMUsed : status = 226 /* RFC 3229, 10.4.1*/ + + const MultipleChoices : status = 300 /* RFC 7231, 6.4.1*/ + const MovedPermanently : status = 301 /* RFC 7231, 6.4.2*/ + const Found : status = 302 /* RFC 7231, 6.4.3*/ + const SeeOther : status = 303 /* RFC 7231, 6.4.4*/ + const NotModified : status = 304 /* RFC 7232, 4.1*/ + const UseProxy : status = 305 /* RFC 7231, 6.4.5*/ + const TemporaryRedirect : status = 307 /* RFC 7231, 6.4.7*/ + const PermanentRedirect : status = 308 /* RFC 7538, 3*/ + + const BadRequest : status = 400 /* RFC 7231, 6.5.1*/ + const Unauthorized : status = 401 /* RFC 7235, 3.1*/ + const PaymentRequired : status = 402 /* RFC 7231, 6.5.2*/ + const Forbidden : status = 403 /* RFC 7231, 6.5.3*/ + const NotFound : status = 404 /* RFC 7231, 6.5.4*/ + const MethodNotAllowed : status = 405 /* RFC 7231, 6.5.5*/ + const NotAcceptable : status = 406 /* RFC 7231, 6.5.6*/ + const ProxyAuthRequired : status = 407 /* RFC 7235, 3.2*/ + const RequestTimeout : status = 408 /* RFC 7231, 6.5.7*/ + const Conflict : status = 409 /* RFC 7231, 6.5.8*/ + const Gone : status = 410 /* RFC 7231, 6.5.9*/ + const LengthRequired : status = 411 /* RFC 7231, 6.5.10*/ + const PreconditionFailed : status = 412 /* RFC 7232, 4.2*/ + const RequestEntityTooLarge : status = 413 /* RFC 7231, 6.5.11*/ + const RequestURITooLong : status = 414 /* RFC 7231, 6.5.12*/ + const UnsupportedMediaType : status = 415 /* RFC 7231, 6.5.13*/ + const RangeNotSatisfiable : status = 416 /* RFC 7233, 4.4*/ + const ExpectationFailed : status = 417 /* RFC 7231, 6.5.14*/ + const Teapot : status = 418 /* RFC 7168, 2.3.3*/ + const UnprocessableEntity : status = 422 /* RFC 4918, 11.2*/ + const Locked : status = 423 /* RFC 4918, 11.3*/ + const FailedDependency : status = 424 /* RFC 4918, 11.4*/ + const UpgradeRequired : status = 426 /* RFC 7231, 6.5.15*/ + const PreconditionRequired : status = 428 /* RFC 6585, 3*/ + const TooManyRequests : status = 429 /* RFC 6585, 4*/ + const HeaderFieldsTooLarge : status = 431 /* RFC 6585, 5*/ + const UnavailableForLegal : status = 451 /* RFC 7725, 3*/ + + const InternalServerError : status = 500 /* RFC 7231, 6.6.1*/ + const NotImplemented : status = 501 /* RFC 7231, 6.6.2*/ + const BadGateway : status = 502 /* RFC 7231, 6.6.3*/ + const ServiceUnavailable : status = 503 /* RFC 7231, 6.6.4*/ + const GatewayTimeout : status = 504 /* RFC 7231, 6.6.5*/ + const VersionNotSupported : status = 505 /* RFC 7231, 6.6.6*/ + const VariantAlsoNegotiates : status = 506 /* RFC 2295, 8.1*/ + const InsufficientStorage : status = 507 /* RFC 4918, 11.5*/ + const LoopDetected : status = 508 /* RFC 5842, 7.2*/ + const NotExtended : status = 510 /* RFC 2774, 7*/ + const NetworkAuthRequired : status = 511 /* RFC 6585, 6*/ +;; diff --git a/lib/http/types.myr b/lib/http/types.myr new file mode 100644 index 0000000..68e2c28 --- /dev/null +++ b/lib/http/types.myr @@ -0,0 +1,83 @@ +use std +use bio + +pkg http = + type status = int + + type session = struct + f : bio.file# + host : byte[:] + port : uint16 + srvname : byte[:] + ua : byte[:] + err : bool + ;; + + + type server = struct + ann : std.announce# + refs : uint32 + quit : bool + ;; + + type url = struct + schema : schema + port : uint16 + host : byte[:] + path : byte[:] + params : (byte[:], byte[:])[:] + ;; + + type err = union + `Ehttp int /* http error */ + `Eunsupp /* unsupported feature */ + `Econn /* connection lost */ + `Ehdr /* invalid header */ + `Eproto /* protocol error */ + `Eshort /* truncated response */ + `Egarbled /* syntax error */ + `Eenc /* encoding error */ + `Ewat /* unknown error */ + ;; + + type schema = union + `Http + `Https + ;; + + type method = union + `Get + `Head + `Put + `Post + `Delete + `Trace + `Options + ;; + + type encoding = union + `Length + `Chunked + `Compress + `Deflate + `Gzip + ;; + + type req = struct + url : url# + hdrs : (byte[:], byte[:])[:] + err : std.option(err) + method : method + ;; + + type resp = struct + status : int + hdrs : (byte[:], byte[:])[:] + len : std.size + err : std.option(err) + reason : byte[:] + body : byte[:] + enc : encoding + ;; + +;; diff --git a/lib/http/url.myr b/lib/http/url.myr new file mode 100644 index 0000000..be17d20 --- /dev/null +++ b/lib/http/url.myr @@ -0,0 +1,238 @@ +use std + +use "types" +use "parse" + +pkg http = + const parseurl : (url : byte[:] -> std.result(url#, err)) + const urlfree : (url : url# -> void) +;; + +const __init__ = { + var u : url# + + u = u + std.fmtinstall(std.typeof(u), urlfmt, [ + ("p", false) + ][:]) +} + +const urlfmt = {sb, ap, opts + var url : url# + var defaultport + var sep + var showhost + + showhost = true + url = std.vanext(ap) + for o : opts + match o + | ("p", ""): showhost = false + | _: std.fatal("unknown param\n") + ;; + ;; + + if showhost + match url.schema + | `Http: + std.sbputs(sb, "http://") + defaultport = 80 + | `Https: + std.sbputs(sb, "https://") + defaultport = 443 + ;; + + std.sbputs(sb, url.host) + if url.port != defaultport + std.sbfmt(sb, ":{}", url.port) + ;; + ;; + + std.sbfmt(sb, url.path) + + if url.params.len > 0 + sep = '?' + for (k, v) : url.params + std.sbfmt(sb, "{}{}={}", sep, k, v) + sep = '&' + ;; + ;; +} + +const parseurl = {url + var schema, host, port, path, params + + match parseschema(&url) + | `std.Ok s: schema = s + | `std.Err e: -> `std.Err e + ;; + + match parsehostname(&url) + | `std.Ok h: host = h + | `std.Err e: -> `std.Err e + ;; + + match parseport(&url) + | `std.Ok p: port = p + | `std.Err e: -> `std.Err e + ;; + + match parsepath(&url) + | `std.Ok p: path = p + | `std.Err e: -> `std.Err e + ;; + + match parseparams(&url) + | `std.Ok p: params = p + | `std.Err e: -> `std.Err e + ;; + + /* todo: params */ + -> `std.Ok std.mk([ + .schema=schema, + .host=std.sldup(host), + .port=port, + .path=std.sldup(path), + .params=params, + ]) +} + +const urlfree = {url + std.slfree(url.host) + std.slfree(url.path) + std.free(url) +} + +const parseschema = {url + if std.chomp(url, "http://") + -> `std.Ok `Http + elif std.chomp(url, "https://") + -> `std.Err `Eunsupp + else + -> `std.Err `Eproto + ;; +} + +const parsehostname = {url + if std.chomp(url, "[") + -> ipv6hostname(url) + else + -> hostname(url) + ;; +} + +const parsepath = {url + var len, p + + if url#.len == 0 + -> `std.Ok "/" + elif std.decode(url#) == '?' + -> `std.Ok "/" + elif std.decode(url#) == '/' + match std.strfind(url#, "?") + | `std.Some i: len = i + | `std.None: len = url#.len + ;; + p = url#[:len] + url# = url#[len:] + -> `std.Ok p + else + -> `std.Err `Egarbled + ;; +} + +const parseparams = {url + var kvp : byte[:][2] + var params + + if url#.len == 0 + -> `std.Ok [][:] + ;; + + match std.decode(url#) + | '?': (_, url#) = std.strstep(url#) + | _: -> `std.Err `Egarbled + ;; + + params = [][:] + for sp : std.bysplit(url#, "&") + if std.bstrsplit(kvp[:], sp, "=").len != 2 + -> `std.Err `Egarbled + ;; + std.slpush(¶ms, (std.sldup(kvp[0]), std.sldup(kvp[1]))) + ;; + + -> `std.Ok params +} + +const hostname = {url + var len, host + + len = 0 + for c : std.bychar(url#) + match c + | ':': break + | '/': break + | '?': break + | chr: + if ishostchar(chr) + len += std.charlen(chr) + else + -> `std.Err `Egarbled + ;; + ;; + ;; + + host = url#[:len] + url# = url#[len:] + -> `std.Ok host +} + +const ipv6hostname = {url -> std.result(byte[:], err) + var ip + + match std.strfind(url#, "]") + | `std.None: -> `std.Err `Egarbled + | `std.Some idx: + ip = url#[:idx] + url# = url#[idx+1:] + match std.ip6parse(url#[:idx]) + | `std.Some _: -> `std.Ok ip + | `std.None: -> `std.Err `Egarbled + ;; + ;; +} + +const parseport = {url + if std.chomp(url, ":") + match parsenumber(url, 10) + | `std.None: -> `std.Err `Egarbled + | `std.Some n: + if n > 65535 + -> `std.Err `Egarbled + else + -> `std.Ok (n : uint16) + ;; + ;; + else + -> `std.Ok 80 + ;; +} + +const ishostchar = {c + if !std.isascii(c) + -> false + else + -> std.isalnum(c) || unreserved(c) || subdelim(c) + ;; +} + +const unreserved = {c + -> c == '.' || c == '-' || c == '_' || c == '~' +} + +const subdelim = {c + -> c == '.' || c == '-' || c == '_' || c == '~' || c == '!' || \ + c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || \ + c == '*' || c == '+' || c == ',' || c == ';' || c == '=' +} |