shithub: mc

Download patch

ref: 735a7d2932d70b5bf232e2d021e6c0c61e03f61a
parent: 38941f5eadd5e3606776d3c0004b997fb53ba399
parent: dd8235ce6d9c45e7039a821bdc86818c63c2823d
author: Ori Bernstein <[email protected]>
date: Sun Sep 10 17:43:59 EDT 2017

Merge ../libhttp

--- /dev/null
+++ b/lib/http/bld.sub
@@ -1,0 +1,21 @@
+bin h {noinst}=
+	h.myr
+
+	lib http
+;;
+
+bin srvdot {noinst}=
+	srvdot.myr
+
+	lib http
+;;
+
+lib http =
+	parse.myr
+	types.myr
+	url.myr
+	client.myr
+	server.myr
+	session.myr
+	status.myr
+;;
--- /dev/null
+++ b/lib/http/client.myr
@@ -1,0 +1,281 @@
+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:
+		ioput(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)
+	| `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.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])
+			| `bio.Eof:
+				std.slfree(buf)
+				-> `std.Err `Eshort
+			| `bio.Err e:
+				std.slfree(buf)
+				-> `std.Err `Econn
+			| `bio.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)
+	| `bio.Err e:	r = `std.Err `Econn
+	| `bio.Eof:	r = `std.Err `Econn
+	| `bio.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)
+}
--- /dev/null
+++ b/lib/http/h.myr
@@ -1,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))
+	;;
+}
+
--- /dev/null
+++ b/lib/http/parse.myr
@@ -1,0 +1,287 @@
+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)
+	| `bio.Err e:
+		err = `Econn
+		goto error
+	| `bio.Eof:	
+		err = `Econn
+		goto error
+	| `bio.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)
+		| `bio.Err e:	
+			err = `Econn
+			goto error
+		| `bio.Eof:
+			err = `Econn
+			goto error
+		| `bio.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)
+	| `bio.Err e:	r.err = `std.Some `Econn
+	| `bio.Eof:	r.err = `std.Some `Econn
+	| `bio.Ok ln:
+		if !parserespstatus(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
+				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)
+	| `bio.Eof:	ret = `std.Err `Econn
+	| `bio.Err e:	ret = `std.Err `Econn
+	| `bio.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
+	;;
+}
+
--- /dev/null
+++ b/lib/http/serve.myr
@@ -1,0 +1,14 @@
+use std
+use bio
+
+use "types"
+use "parse"
+
+pkg http =
+	const readreq	: (srv	: server#	-> req#)
+;;
+
+
+const readreq = {srv : server#
+	-> req
+}
--- /dev/null
+++ b/lib/http/server.myr
@@ -1,0 +1,110 @@
+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
+	var afd
+
+	match std.announce(ds)
+	| `std.Ok f:	afd = f
+	| `std.Err e:	-> `std.Err `Econn
+	;;
+
+	match std.listen(afd)
+	| `std.Err e:	-> `std.Err `Econn
+	| `std.Ok lfd:
+		std.close(afd)
+		-> `std.Ok std.mk([
+			.refs=1,
+			.lfd=lfd,
+			.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()
+	bio.put(s.f, "HTTP/1.1 {} {}\r\n", resp.status, statusstr(resp.status))
+	bio.put(s.f, "Content-Length: {}\r\n", resp.body.len)
+	bio.put(s.f, "Encoding: {}\r\n", resp.enc)
+	for (k, v) : resp.hdrs
+		bio.put(s.f, "{}: {}\r\n", k, v)
+	;;
+	bio.put(s.f, "\r\n")
+	bio.write(s.f, resp.body)
+	bio.flush(s.f)
+}
+
+const statusstr = {st
+	match st
+	| 200:	-> "OK"
+	| 404:	-> "Not Found"
+	| 503:	-> "Internal Error"
+	| _:	-> "Bad State"
+	;;
+}
+
+const shutdown = {srv
+	std.close(srv.lfd)
+	srv.quit = true
+
+}
+
+
+const waitconn = {srv
+	match std.accept(srv.lfd)
+	| `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)
+	;;
+}
--- /dev/null
+++ b/lib/http/session.myr
@@ -1,0 +1,69 @@
+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 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)
+	| `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/srvdot.myr
@@ -1,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)
+}
--- /dev/null
+++ b/lib/http/status.myr
@@ -1,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*/
+;;
--- /dev/null
+++ b/lib/http/types.myr
@@ -1,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
+		lfd	: std.fd
+		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
+	;;
+
+;;
--- /dev/null
+++ b/lib/http/url.myr
@@ -1,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(&params, (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 ==  '='
+}