τau / net/http /

http.tau

source
/Users/niconex/Documents/tau/stdlib/net/http/http.tau
1# http - HTTP/1.1 client and server, in the shape of Go's net/http.2#3# A client sends a request and reads a response; a server accepts connections4# and hands each one to a handler, which is any function of (w, r). Headers5# are kept in a map whose keys are lowercase, so looking one up doesn't6# depend on how the other side chose to spell it.7#8# What is not here: TLS, chunked transfer encoding, keep alive. Every9# connection carries one exchange and is closed.1011net = import("net")12strings = import("strings")13strconv = import("strconv")14bufio = import("bufio")1516# ========== Status Codes ==========1718StatusOK = 20019StatusCreated = 20120StatusAccepted = 20221StatusNoContent = 20422StatusMovedPermanently = 30123StatusFound = 30224StatusSeeOther = 30325StatusNotModified = 30426StatusTemporaryRedirect = 30727StatusPermanentRedirect = 30828StatusBadRequest = 40029StatusUnauthorized = 40130StatusForbidden = 40331StatusNotFound = 40432StatusMethodNotAllowed = 40533StatusRequestTimeout = 40834StatusConflict = 40935StatusPayloadTooLarge = 41336StatusUnsupportedMediaType = 41537StatusTooManyRequests = 42938StatusInternalServerError = 50039StatusNotImplemented = 50140StatusBadGateway = 50241StatusServiceUnavailable = 50342StatusGatewayTimeout = 5044344statusText = {45	200: "OK",46	201: "Created",47	202: "Accepted",48	204: "No Content",49	301: "Moved Permanently",50	302: "Found",51	303: "See Other",52	304: "Not Modified",53	307: "Temporary Redirect",54	308: "Permanent Redirect",55	400: "Bad Request",56	401: "Unauthorized",57	403: "Forbidden",58	404: "Not Found",59	405: "Method Not Allowed",60	408: "Request Timeout",61	409: "Conflict",62	413: "Payload Too Large",63	415: "Unsupported Media Type",64	429: "Too Many Requests",65	500: "Internal Server Error",66	501: "Not Implemented",67	502: "Bad Gateway",68	503: "Service Unavailable",69	504: "Gateway Timeout"70}7172# StatusText returns the reason phrase of a status code, empty when the code73# is not one of the known ones.74StatusText = fn(code) {75	if statusText[code] == null {76		return ""77	}78	return statusText[code]79}8081# ========== Methods ==========8283MethodGet = "GET"84MethodHead = "HEAD"85MethodPost = "POST"86MethodPut = "PUT"87MethodPatch = "PATCH"88MethodDelete = "DELETE"89MethodOptions = "OPTIONS"9091# ========== URLs ==========9293# ParseURL takes a URL apart: Scheme, Host with its port, Path, RawQuery and94# Query, which is the query string already split into a map.95ParseURL = fn(rawurl) {96	scheme = "http"97	rest = rawurl9899	if strings.HasPrefix(rawurl, "http://") {100		rest = slice(rawurl, 7, len(rawurl))101	} else if strings.HasPrefix(rawurl, "https://") {102		return error("http: https is not supported")103	} else if strings.Contains(rawurl, "://") {104		return error("http: unsupported scheme in \"{rawurl}\"")105	}106107	host = rest108	path = "/"109110	if (i = strings.Index(rest, "/")) != -1 {111		host = slice(rest, 0, i)112		path = slice(rest, i, len(rest))113	}114115	if host == "" {116		return error("http: no host in \"{rawurl}\"")117	}118119	query = ""120	if (i = strings.Index(path, "?")) != -1 {121		query = slice(path, i + 1, len(path))122		path = slice(path, 0, i)123	}124125	u = new()126	u.Scheme = scheme127	u.Host = if strings.Index(host, ":") == -1 { host + ":80" } else { host }128	u.Hostname = net.SplitHostPort(u.Host).Host129	u.Path = path130	u.RawQuery = query131	u.Query = ParseQuery(query)132	return u133}134135# ParseQuery reads a query string into a map. A key without a value maps to136# the empty string, the way an empty field does.137ParseQuery = fn(raw) {138	out = {}139	if raw == "" {140		return out141	}142143	pairs = strings.Split(raw, "&")144	for i = 0; i < len(pairs); ++i {145		p = pairs[i]146		if p == "" {147			continue148		}149150		if (j = strings.Index(p, "=")) == -1 {151			out[unescape(p)] = ""152		} else {153			out[unescape(slice(p, 0, j))] = unescape(slice(p, j + 1, len(p)))154		}155	}156	return out157}158159hexValue = fn(c) {160	if c >= "0" && c <= "9" {161		return bytes(c)[0] - 48162	}163	if c >= "a" && c <= "f" {164		return bytes(c)[0] - 87165	}166	if c >= "A" && c <= "F" {167		return bytes(c)[0] - 55168	}169	return -1170}171172# unescape turns %XX back into the byte it stands for, and + into a space.173unescape = fn(s) {174	out = []175	for i = 0; i < len(s); ++i {176		c = s[i]177178		if c == "+" {179			out = append(out, 32)180		} else if c == "%" && i + 2 < len(s) && hexValue(s[i + 1]) >= 0 && hexValue(s[i + 2]) >= 0 {181			out = append(out, hexValue(s[i + 1]) << 4 | hexValue(s[i + 2]))182			i = i + 2183		} else {184			out = append(out, bytes(c)[0])185		}186	}187	return string(bytes(out))188}189190unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"191hexDigits = "0123456789ABCDEF"192193# Escape writes a string so that it can travel inside a URL.194Escape = fn(s) {195	b = bytes(s)196	out = ""197198	for i = 0; i < len(b); ++i {199		c = string(bytes([b[i]]))200		if strings.Contains(unreserved, c) {201			out = out + c202		} else {203			out = out + "%" + hexDigits[b[i] >> 4] + hexDigits[b[i] & 0x0f]204		}205	}206	return out207}208209# ========== Headers ==========210211# newHeader returns an empty header map with the two ways of reaching into212# it: Get and Set, which don't care about case.213newHeader = fn() {{} }214215# HeaderGet returns the value of a header, or an empty string.216HeaderGet = fn(h, name) {217	v = h[strings.ToLower(name)]218	if v == null {219		return ""220	}221	return v222}223224HeaderSet = fn(h, name, value) {225	h[strings.ToLower(name)] = string(value)226	return h227}228229# canonical writes a header name the way HTTP does: Content-Type.230canonical = fn(name) {231	parts = strings.Split(strings.ToLower(name), "-")232	for i = 0; i < len(parts); ++i {233		if len(parts[i]) > 0 {234			parts[i] = strings.ToUpper(slice(parts[i], 0, 1)) + slice(parts[i], 1, len(parts[i]))235		}236	}237	return strings.Join(parts, "-")238}239240# readHeaders reads the header lines up to the empty one that ends them.241readHeaders = fn(r) {242	h = newHeader()243244	for {245		line = r.ReadLine()246		if line == null || line == "" {247			return h248		}249250		if (i = strings.Index(line, ":")) != -1 {251			name = strings.ToLower(strings.TrimSpace(slice(line, 0, i)))252			h[name] = strings.TrimSpace(slice(line, i + 1, len(line)))253		}254	}255}256257# readBody returns as many bytes as Content-Length says. Without that header258# there is a body only if the sender is going to close the connection to mark259# its end, which is what toEnd is for: true for a response, false for a260# request, where waiting for a close that never comes would hang the server.261readBody = fn(r, h, toEnd) {262	length = HeaderGet(h, "content-length")263264	if length == "" {265		if toEnd {266			return string(r.ReadAll())267		}268		return ""269	}270	if failed(n = strconv.Atoi(length)) {271		return error("http: bad Content-Length \"{length}\"")272	}273	if n <= 0 {274		return ""275	}276	return string(r.Read(n))277}278279# ========== Requests and Responses ==========280281# NewRequest builds a request. Body may be null for the methods that carry282# none.283NewRequest = fn(method, url, body) {284	if failed(u = ParseURL(url)) {285		return u286	}287288	req = new()289	req.Method = if method == null { MethodGet } else { strings.ToUpper(method) }290	req.URL = url291	req.Path = u.Path292	req.RawQuery = u.RawQuery293	req.Query = u.Query294	req.Host = u.Host295	req.Header = newHeader()296	req.Body = if body == null { "" } else { string(body) }297	return req298}299300newResponse = fn() {301	resp = new()302	resp.StatusCode = 0303	resp.Status = ""304	resp.Proto = "HTTP/1.1"305	resp.Header = newHeader()306	resp.Body = ""307	return resp308}309310# ========== Client ==========311312# NewClient returns a client. Timeout is in milliseconds and may be null.313NewClient = fn(timeout) {314	client = new()315	client.Timeout = if timeout == null { 30000 } else { timeout }316317	# Do sends a request and returns the response.318	client.Do = fn(req) {319		if failed(u = ParseURL(req.URL)) {320			return u321		}322		if failed(conn = net.Dial("tcp", u.Host)) {323			return conn324		}325		conn.SetTimeout(client.Timeout)326327		target = u.Path328		if u.RawQuery != "" {329			target = target + "?" + u.RawQuery330		}331332		out = "{req.Method} {target} HTTP/1.1\r\n"333		out = out + "Host: {u.Hostname}\r\n"334		out = out + "Connection: close\r\n"335336		if HeaderGet(req.Header, "user-agent") == "" {337			out = out + "User-Agent: tau-http/1.0\r\n"338		}339340		ks = keys(req.Header)341		for i = 0; i < len(ks); ++i {342			out = out + "{canonical(ks[i])}: {req.Header[ks[i]]}\r\n"343		}344345		if len(req.Body) > 0 {346			out = out + "Content-Length: {len(req.Body)}\r\n"347		}348349		out = out + "\r\n" + req.Body350351		if failed(n = conn.Write(out)) {352			conn.Close()353			return n354		}355356		resp = readResponse(conn)357		conn.Close()358		return resp359	}360361	client.Get = fn(url) {362		if failed(req = NewRequest(MethodGet, url, null)) {363			return req364		}365		return client.Do(req)366	}367368	client.Post = fn(url, contentType, body) {369		if failed(req = NewRequest(MethodPost, url, body)) {370			return req371		}372		HeaderSet(req.Header, "content-type", if contentType == null { "text/plain" } else { contentType })373		return client.Do(req)374	}375376	client.Head = fn(url) {377		if failed(req = NewRequest(MethodHead, url, null)) {378			return req379		}380		return client.Do(req)381	}382383	return client384}385386# readResponse reads a whole response off a connection.387readResponse = fn(conn) {388	r = bufio.NewReader(conn, 4096)389390	line = r.ReadLine()391	if line == null || line == "" {392		return error("http: empty response")393	}394395	parts = strings.SplitN(line, " ", 3)396	if len(parts) < 2 {397		return error("http: malformed status line \"{line}\"")398	}399	if failed(code = strconv.Atoi(parts[1])) {400		return error("http: malformed status code \"{parts[1]}\"")401	}402403	resp = newResponse()404	resp.Proto = parts[0]405	resp.StatusCode = code406	resp.Status = if len(parts) > 2 { parts[2] } else { StatusText(code) }407	resp.Header = readHeaders(r)408409	if failed(body = readBody(r, resp.Header, true)) {410		return body411	}412	resp.Body = body413	return resp414}415416DefaultClient = NewClient(null)417418Get = fn(url) { DefaultClient.Get(url) }419Post = fn(url, contentType, body) { DefaultClient.Post(url, contentType, body) }420Head = fn(url) { DefaultClient.Head(url) }421422# ========== Server ==========423424# newResponseWriter is what a handler writes into. Nothing reaches the425# connection until the handler is done: the status line comes first, and a426# handler is free to change its mind about it until it writes something.427newResponseWriter = fn() {428	w = new()429	w.statusCode = StatusOK430	w.headers = newHeader()431	w.body = ""432433	w.Header = fn() { w.headers }434	w.WriteHeader = fn(code) { w.statusCode = code }435436	w.Write = fn(data) {437		s = string(data)438		w.body = w.body + s439		return len(s)440	}441442	w.WriteString = fn(s) { w.Write(s) }443444	# response is the bytes to send back.445	w.response = fn() {446		out = "HTTP/1.1 {w.statusCode} {StatusText(w.statusCode)}\r\n"447448		HeaderSet(w.headers, "content-length", len(w.body))449		if HeaderGet(w.headers, "content-type") == "" {450			HeaderSet(w.headers, "content-type", "text/plain; charset=utf-8")451		}452		HeaderSet(w.headers, "connection", "close")453454		ks = keys(w.headers)455		for i = 0; i < len(ks); ++i {456			out = out + "{canonical(ks[i])}: {w.headers[ks[i]]}\r\n"457		}458459		return out + "\r\n" + w.body460	}461462	return w463}464465# readRequest reads a request off a connection.466readRequest = fn(conn) {467	r = bufio.NewReader(conn, 4096)468469	line = r.ReadLine()470	if line == null || line == "" {471		return error("http: empty request")472	}473474	parts = strings.Split(line, " ")475	if len(parts) < 2 {476		return error("http: malformed request line \"{line}\"")477	}478479	target = parts[1]480	query = ""481	if (i = strings.Index(target, "?")) != -1 {482		query = slice(target, i + 1, len(target))483		target = slice(target, 0, i)484	}485486	req = new()487	req.Method = parts[0]488	req.Proto = if len(parts) > 2 { parts[2] } else { "HTTP/1.0" }489	req.Path = target490	req.URL = target491	req.RawQuery = query492	req.Query = ParseQuery(query)493	req.Header = readHeaders(r)494	req.Host = HeaderGet(req.Header, "host")495	req.RemoteAddr = conn.RemoteAddr496497	if failed(body = readBody(r, req.Header, false)) {498		return body499	}500	req.Body = body501	return req502}503504# NewServeMux returns a multiplexer. A pattern ending in "/" matches every505# path below it, anything else matches exactly, and the longest pattern that506# matches wins, as in Go.507NewServeMux = fn() {508	mux = new()509	mux.patterns = []510	mux.handlers = {}511512	mux.Handle = fn(pattern, handler) {513		if mux.handlers[pattern] == null {514			mux.patterns = append(mux.patterns, pattern)515		}516		mux.handlers[pattern] = handler517		return null518	}519520	mux.HandleFunc = fn(pattern, handler) { mux.Handle(pattern, handler) }521522	# Handler returns the handler for a path, or null when none matches.523	mux.Handler = fn(path) {524		best = ""525		for i = 0; i < len(mux.patterns); ++i {526			p = mux.patterns[i]527528			matches = if strings.HasSuffix(p, "/") { strings.HasPrefix(path, p) } else { path == p }529			if matches && len(p) > len(best) {530				best = p531			}532		}533534		if best == "" {535			return null536		}537		return mux.handlers[best]538	}539540	mux.ServeHTTP = fn(w, r) {541		h = mux.Handler(r.Path)542		if h == null {543			NotFound(w, r)544			return null545		}546		return h(w, r)547	}548549	return mux550}551552DefaultServeMux = NewServeMux()553554Handle = fn(pattern, handler) { DefaultServeMux.Handle(pattern, handler) }555HandleFunc = fn(pattern, handler) { DefaultServeMux.HandleFunc(pattern, handler) }556557# NotFound is the handler for a path nothing was registered for.558NotFound = fn(w, r) {559	w.WriteHeader(StatusNotFound)560	w.Write("404 page not found\n")561	return null562}563564# Error replies with a message and a status code.565Error = fn(w, msg, code) {566	w.WriteHeader(code)567	w.Write(string(msg) + "\n")568	return null569}570571# Redirect replies with a redirect to url.572Redirect = fn(w, r, url, code) {573	HeaderSet(w.Header(), "location", url)574	w.WriteHeader(if code == null { StatusFound } else { code })575	w.Write("")576	return null577}578579# serve reads one exchange off a connection and answers it.580serve = fn(conn, handler) {581	w = newResponseWriter()582583	if failed(req = readRequest(conn)) {584		w.WriteHeader(StatusBadRequest)585		w.Write("400 bad request\n")586	} else if type(handler) == "closure" {587		handler(w, req)588	} else {589		handler.ServeHTTP(w, req)590	}591592	conn.Write(w.response())593	conn.Close()594	return null595}596597# NewServer returns a server. Handler is a mux or any function of (w, r).598NewServer = fn(addr, handler) {599	server = new()600	server.Addr = addr601	server.Handler = if handler == null { DefaultServeMux } else { handler }602	server.running = false603604	# ListenAndServe accepts connections until Close is called, one tau605	# routine per connection so that a slow client holds up nobody.606	server.ListenAndServe = fn() {607		if failed(ln = net.Listen("tcp", server.Addr)) {608			return ln609		}610611		server.ln = ln612		server.running = true613614		for server.running {615			if failed(conn = ln.Accept()) {616				if !server.running {617					return null618				}619				continue620			}621			tau serve(conn, server.Handler)622		}623624		return null625	}626627	server.Close = fn() {628		server.running = false629		if server.ln != null {630			return server.ln.Close()631		}632		return null633	}634635	return server636}637638# ListenAndServe serves handler on addr until something goes wrong.639ListenAndServe = fn(addr, handler) {640	return NewServer(addr, handler).ListenAndServe()641}