ref: 22d71e9e0471402b4d9e918f4f10683137ee9121
author: Ori Bernstein <ori@eigenstate.org>
date: Sun Jun 19 09:58:58 EDT 2016
Initial commit.
--- /dev/null
+++ b/lib/http/bld.sub
@@ -1,0 +1,14 @@
+bin h =
+ main.myr
+
+ lib http
+;;
+
+lib http =
+ parse.myr
+ types.myr
+ url.myr
+ verbs.myr
+ session.myr
+
+;;
--- /dev/null
+++ b/lib/http/main.myr
@@ -1,0 +1,61 @@
+use std
+use http
+
+const main = {args
+ var data, method, hdr
+ var s, u, r
+ var cmd
+
+ cmd = std.optparse(args, &[
+ .argdesc = "url...",
+ .opts = [
+ [.opt='m', .arg="method", .desc="http method to use"],
+ [.opt='d', .arg="data", .desc="data to put in request body"],
+ [.opt='H', .desc="show headers"],
+ ][:]
+ ])
+
+ hdr = false
+ method = "get"
+ data = ""
+ for opt in cmd.opts
+ match opt
+ | ('m', m): method = m
+ | ('d', d): data = d
+ | ('h', ""): hdr = true
+ | _: std.die("unreachable")
+ ;;
+ ;;
+
+
+ for url in cmd.args
+ u = std.try(http.parseurl(url))
+ std.put("URL: {}\n", u#)
+ s = std.try(http.mksession(u.host))
+
+ match method
+ | "get": r = http.get(s, u.path)
+ | "head": r = http.head(s, u.path)
+ | "delete": r = http.delete(s, u.path)
+ | "trace": r = http.trace(s, u.path)
+ | "options": r = http.trace(s, u.path)
+ | "put": r = http.put(s, u.path, data)
+ | "post": r = http.post(s, u.path, data)
+ | unknown: std.fatal("unknown method '{}'\n", unknown)
+ ;;
+
+ match r
+ | `std.Ok resp:
+ std.put("status: {}\n", resp.status)
+ for h in resp.hdrs
+ std.put("{}\n", h)
+ ;;
+ std.put("{}\n", resp.body)
+ http.freeresp(resp)
+ | `std.Fail e:
+ std.put("{}\n", e)
+ ;;
+ http.urlfree(u)
+ ;;
+}
+
--- /dev/null
+++ b/lib/http/parse.myr
@@ -1,0 +1,176 @@
+use std
+use bio
+
+use "types"
+
+pkg http =
+ 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 parseresp = {s, r
+ match bio.readln(s.f)
+ | `bio.Err e: r.err = `std.Some `Econn
+ | `bio.Eof: r.err = `std.Some `Econn
+ | `bio.Ok ln:
+ if !parsestatus(s, r, ln)
+ std.slfree(ln)
+ -> false
+ ;;
+ std.slfree(ln)
+ ;;
+
+ while true
+ match bio.readln(s.f)
+ | `bio.Err e: r.err = `std.Some `Econn
+ | `bio.Eof: r.err = `std.Some `Econn
+ | `bio.Ok ln:
+ if std.strstrip(ln).len == 0
+ break
+ ;;
+ if !parsehdr(s, r, ln)
+ 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.Fail e:
+ r.err = `std.Some e
+ -> false
+ ;;
+ -> true
+
+}
+
+const parsechunksz = {s
+ var ret, str
+
+ match bio.readln(s.f)
+ | `bio.Eof: ret = `std.Fail `Econn
+ | `bio.Err e: ret = `std.Fail `Econn
+ | `bio.Ok ln:
+ str = ln
+ match parsenumber(&str, 16)
+ | `std.Some n: ret = `std.Ok (n : std.size)
+ | `std.None:
+ ret = `std.Fail `Eproto
+ ;;
+ std.slfree(ln)
+ ;;
+ -> ret
+}
+
+const parsestatus = {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, r, 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.slpush(&r.hdrs, (key, val))
+ -> true
+ | `std.None:
+ r.err = `std.Some `Ehdr
+ -> false
+ ;;
+}
+
+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 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.Fail `Eenc
+ ;;
+}
+
+const findhdr = {r, name
+ for (k, v) in r.hdrs
+ if std.strcaseeq(k, name)
+ -> `std.Some v
+ ;;
+ ;;
+ -> `std.None
+}
+
+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
+ ;;
+}
+
--- /dev/null
+++ b/lib/http/session.myr
@@ -1,0 +1,56 @@
+use std
+use bio
+
+use "types"
+
+pkg http =
+ const mksession : (host : byte[:] -> std.result(session#, err))
+ const freesession : (s : session# -> void)
+
+ pkglocal const ioput : (s : session#, fmt : byte[:], args : ... -> bool)
+ pkglocal const ioflush : (s : session# -> void)
+;;
+
+const mksession = {host
+ var s, sess
+
+ s = std.fmt("tcp!{}!80", host)
+ match std.dial(s)
+ | `std.Fail e: sess = `std.Fail `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)
+ ])
+ ;;
+ -> sess
+}
+
+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)
+ std.putv(fmt, &ap)
+ ap = std.vastart(&args)
+ match bio.putv(s.f, fmt, &ap)
+ | `bio.Ok _: /* nothing */
+ | `bio.Err _: s.err = true
+ | `bio.Eof: s.err = true
+ ;;
+ -> s.err
+}
+
+const ioflush = {s
+ bio.flush(s.f)
+}
--- /dev/null
+++ b/lib/http/types.myr
@@ -1,0 +1,70 @@
+use std
+use bio
+
+pkg http =
+ type session = struct
+ f : bio.file#
+ host : byte[:]
+ ua : byte[:]
+ err : bool
+ ;;
+
+ type url = struct
+ schema : schema
+ port : int
+ host : byte[:]
+ path : byte[:]
+ ;;
+
+
+ type err = union
+ `Ewat
+ `Eunsupp
+ `Econn
+ `Ehttp int
+ `Ehdr
+ `Eproto
+ `Eshort
+ `Esyntax
+ `Eenc
+ ;;
+
+ 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
+ hdrs : (byte[:], byte[:])[:]
+ method : method
+ ;;
+
+ type resp = struct
+ status : int
+ hdrs : (byte[:], byte[:])[:]
+ len : std.size
+ err : std.option(err)
+ reason : byte[:]
+ body : byte[:]
+ enc : encoding
+ ;;
+
+;;
--- /dev/null
+++ b/lib/http/url.myr
@@ -1,0 +1,140 @@
+use std
+
+use "types"
+use "parse"
+
+pkg http =
+ const parseurl : (url : byte[:] -> std.result(url#, err))
+ const urlfree : (url : url# -> void)
+;;
+
+const parseurl = {url
+ var schema, host, path, port
+
+ match parseschema(&url)
+ | `std.Ok s: schema = s
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ match parsehostname(&url)
+ | `std.Ok h: host = h
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ match parseport(&url)
+ | `std.Ok p: port = p
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ match parsepath(&url)
+ | `std.Ok p: path = p
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ /* todo: params */
+ -> `std.Ok std.mk([
+ .schema=schema,
+ .host=std.sldup(host),
+ .path=std.sldup(path),
+ ])
+}
+
+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.Fail `Eunsupp
+ else
+ -> `std.Fail `Eproto
+ ;;
+}
+
+const parsehostname = {url
+ if std.chomp(url, "[")
+ -> ipv6hostname(url)
+ else
+ -> hostname(url)
+ ;;
+}
+
+const parsepath = {url
+ if url#.len == 0
+ -> `std.Ok "/"
+ elif std.decode(url#) == '/'
+ -> `std.Ok url#
+ else
+ -> `std.Fail `Esyntax
+ ;;
+}
+
+const hostname = {url
+ var len, host
+
+ len = 0
+ for c in std.bychar(url#)
+ match c
+ | ':': break
+ | '/': break
+ | chr:
+ if ishostchar(chr)
+ len += std.charlen(chr)
+ else
+ -> `std.Fail `Esyntax
+ ;;
+ ;;
+ ;;
+
+ 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.Fail `Esyntax
+ | `std.Some idx:
+ ip = url#[:idx]
+ url# = url#[idx+1:]
+ match std.ip6parse(url#[:idx])
+ | `std.Some _: -> `std.Ok ip
+ | `std.None: -> `std.Fail `Esyntax
+ ;;
+ ;;
+}
+
+const parseport = {url
+ if std.chomp(url, ":")
+ match parsenumber(url, 10)
+ | `std.Some n: -> `std.Ok n
+ | `std.None: -> `std.Fail `Esyntax
+ ;;
+ 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 == '='
+}
--- /dev/null
+++ b/lib/http/verbs.myr
@@ -1,0 +1,262 @@
+use std
+use bio
+
+use "types"
+use "session"
+use "parse"
+
+pkg http =
+ const get : (s : session#, path : byte[:] -> std.result(resp#, err))
+ const head : (s : session#, path : byte[:] -> std.result(resp#, err))
+ const put : (s : session#, path : byte[:], data : byte[:] -> std.result(resp#, err))
+ const post : (s : session#, path : byte[:], data : byte[:] -> std.result(resp#, err))
+ const delete : (s : session#, path : byte[:] -> std.result(resp#, err))
+ const options : (s : session#, path : byte[:] -> std.result(resp#, err))
+ const trace : (s : session#, path : byte[:] -> std.result(resp#, err))
+
+ const freeresp : (r : resp# -> void)
+;;
+
+const get = {s, path
+ match request(s, `Get, path, [][:], `std.None)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, true)
+}
+
+const head = {s, path
+ match request(s, `Head, path, [][:], `std.None)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, false)
+}
+
+const put = {s, path, data
+ match request(s, `Put, path, [][:], `std.Some data)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, true)
+}
+
+const post = {s, path, data
+ match request(s, `Post, path, [][:], `std.Some data)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, true)
+}
+
+const delete = {s, path
+ match request(s, `Delete, path, [][:], `std.None)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, true)
+}
+
+const options = {s, path
+ match request(s, `Options, path, [][:], `std.None)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail e
+ ;;
+
+ -> response(s, true)
+}
+
+const trace = {s, path
+ match request(s, `Options, path, [][:], `std.None)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e: -> `std.Fail 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.Fail e: -> `std.Fail e
+ ;;
+ ;;
+ else
+ match resp.err
+ | `std.Some e: -> `std.Fail e
+ | `std.None: -> `std.Fail `Ewat
+ ;;
+ ;;
+ -> `std.Ok resp
+}
+
+const request = {s, method, path, hdrs : (byte[:], byte[:])[:], data
+ /* status */
+ ioput(s, "{} {} HTTP/1.1\r\n", method, path)
+
+ /* 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) in hdrs
+ ioput(s, "{}: {}\r\n", k, v)
+ ;;
+ ioput(s, "\r\n")
+
+ /* body */
+ match data
+ | `std.None: /* nothing to do */
+ | `std.Some d:
+ ioput(s, d)
+ ioput(s, "\r\n")
+ ;;
+ ioflush(s)
+
+ if s.err
+ -> `std.Fail `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)
+ | `bio.Err e: goto shortread
+ | `bio.Eof: goto shortread
+ | `bio.Ok rd:
+ if rd.len != r.len
+ goto shortread
+ ;;
+ ;;
+ -> `std.Ok buf
+:shortread
+ std.slfree(buf)
+ -> `std.Fail `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.Fail e:
+ std.slfree(buf)
+ -> `std.Fail e
+ | `std.Ok 0:
+ break
+ | `std.Ok sz:
+ std.slgrow(&buf, buf.len + sz)
+ match bio.read(s.f, buf[len:len + sz])
+ | `bio.Eof:
+ std.slfree(buf)
+ -> `std.Fail `Eshort
+ | `bio.Err e:
+ std.slfree(buf)
+ -> `std.Fail `Econn
+ | `bio.Ok str:
+ if str.len != sz
+ std.slfree(buf)
+ -> `std.Fail `Eshort
+ ;;
+ len += sz
+ match checkendln(s)
+ | `std.Ok _: /* nothing */
+ | `std.Fail e:
+ std.slfree(buf)
+ -> `std.Fail e
+ ;;
+ ;;
+ ;;
+ ;;
+ -> `std.Ok buf
+}
+
+const checkendln = {s
+ var r
+
+ match bio.readln(s.f)
+ | `bio.Err e: r = `std.Fail `Econn
+ | `bio.Eof: r = `std.Fail `Econn
+ | `bio.Ok crlf:
+ if std.strstrip(crlf).len == 0
+ r = `std.Ok void
+ else
+ r = `std.Fail `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) in r.hdrs
+ std.slfree(k)
+ std.slfree(v)
+ ;;
+ std.slfree(r.reason)
+ std.free(r)
+}