From 42b7789a9277e7626e4ddaadcbaefb7689d9d8d4 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Wed, 24 Sep 2008 15:26:55 -0700 Subject: [PATCH] early HTTP library and trivial server R=r OCL=15777 CL=15791 --- src/lib/http/Makefile | 59 ++++++++ src/lib/http/conn.go | 60 ++++++++ src/lib/http/request.go | 300 ++++++++++++++++++++++++++++++++++++++++ src/lib/http/server.go | 65 +++++++++ src/lib/http/triv.go | 30 ++++ src/lib/http/url.go | 178 ++++++++++++++++++++++++ 6 files changed, 692 insertions(+) create mode 100644 src/lib/http/Makefile create mode 100644 src/lib/http/conn.go create mode 100644 src/lib/http/request.go create mode 100644 src/lib/http/server.go create mode 100644 src/lib/http/triv.go create mode 100644 src/lib/http/url.go diff --git a/src/lib/http/Makefile b/src/lib/http/Makefile new file mode 100644 index 00000000000..12153c40e37 --- /dev/null +++ b/src/lib/http/Makefile @@ -0,0 +1,59 @@ +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +# DO NOT EDIT. Automatically generated by gobuild. +# gobuild -m http conn.go request.go server.go url.go +O=6 +GC=$(O)g +CC=$(O)c -w +AS=$(O)a +AR=$(O)ar + +PKG=$(GOROOT)/pkg/http.a + +install: $(PKG) + +nuke: clean + rm -f $(PKG) + +clean: + rm -f *.$O *.a + +%.$O: %.go + $(GC) $*.go + +%.$O: %.c + $(CC) $*.c + +%.$O: %.s + $(AS) $*.s + + +O1=\ + url.$O\ + +O2=\ + request.$O\ + +O3=\ + conn.$O\ + +O4=\ + server.$O\ + +$(PKG): a1 a2 a3 a4 +a1: $(O1) + $(AR) grc $(PKG) $(O1) +a2: $(O2) + $(AR) grc $(PKG) $(O2) +a3: $(O3) + $(AR) grc $(PKG) $(O3) +a4: $(O4) + $(AR) grc $(PKG) $(O4) + +$(O1): nuke +$(O2): a1 +$(O3): a2 +$(O4): a3 + diff --git a/src/lib/http/conn.go b/src/lib/http/conn.go new file mode 100644 index 00000000000..ad1b7cc8660 --- /dev/null +++ b/src/lib/http/conn.go @@ -0,0 +1,60 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http + +import ( + "io"; + "bufio"; + "http"; + "os" +) + +// Read/write/close interface. +type RWC interface { + Read(p *[]byte) (n int, err *os.Error); + Write(p *[]byte) (n int, err *os.Error); + Close() *os.Error +} + +// Active HTTP connection (server side). +export type Conn struct { + rwc RWC; + br *bufio.BufRead; + bw *bufio.BufWrite; + close bool; + chunking bool; +} + +// Create new connection from rwc. +export func NewConn(rwc RWC) (c *Conn, err *os.Error) { + c = new(Conn); + c.rwc = rwc; + if c.br, err = bufio.NewBufRead(rwc); err != nil { + return nil, err + } + if c.bw, err = bufio.NewBufWrite(rwc); err != nil { + return nil, err + } + return c, nil +} + +// Read next request from connection. +func (c *Conn) ReadRequest() (req *Request, err *os.Error) { + if req, err = ReadRequest(c.br); err != nil { + return nil, err + } + + // TODO: Proper handling of (lack of) Connection: close, + // and chunked transfer encoding on output. + c.close = true + return req, nil +} + +// Close the connection. +func (c *Conn) Close() { + c.bw.Flush(); + c.rwc.Close(); +} + diff --git a/src/lib/http/request.go b/src/lib/http/request.go new file mode 100644 index 00000000000..bed911eb4c1 --- /dev/null +++ b/src/lib/http/request.go @@ -0,0 +1,300 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// HTTP Request reading and parsing. + +package http + +import ( + "bufio"; + "http"; + "os"; + "strings" +) + +const ( + MaxLineLength = 1024; // assumed < bufio.DefaultBufSize + MaxValueLength = 1024; + MaxHeaderLines = 1024; +) + +export var ( + LineTooLong = os.NewError("http header line too long"); + ValueTooLong = os.NewError("http header value too long"); + HeaderTooLong = os.NewError("http header too long"); + BadHeader = os.NewError("malformed http header"); + BadRequest = os.NewError("invalid http request"); + BadHTTPVersion = os.NewError("unsupported http version"); +) + +// HTTP Request +export type Request struct { + method string; // GET, PUT,etc. + rawurl string; + url *URL; // URI after GET, PUT etc. + proto string; // "HTTP/1.0" + pmajor int; // 1 + pminor int; // 0 + + header *map[string] string; + + close bool; + host string; + referer string; + useragent string; +} + +// Read a line of bytes (up to \n) from b. +// Give up if the line exceeds MaxLineLength. +// The returned bytes are a pointer into storage in +// the bufio, so they are only valid until the next bufio read. +func ReadLineBytes(b *bufio.BufRead) (p *[]byte, err *os.Error) { + if p, err = b.ReadLineSlice('\n'); err != nil { + return nil, err + } + if len(p) >= MaxLineLength { + return nil, LineTooLong + } + + // Chop off trailing white space. + var i int + for i = len(p); i > 0; i-- { + if c := p[i-1]; c != ' ' && c != '\r' && c != '\t' && c != '\n' { + break + } + } + return p[0:i], nil +} + +// ReadLineByte, but convert the bytes into a string. +func ReadLine(b *bufio.BufRead) (s string, err *os.Error) { + p, e := ReadLineBytes(b) + if e != nil { + return "", e + } + return string(p), nil +} + +// Read a key/value pair from b. +// A key/value has the form Key: Value\r\n +// and the Value can continue on multiple lines if each continuation line +// starts with a space. +func ReadKeyValue(b *bufio.BufRead) (key, value string, err *os.Error) { + line, e := ReadLineBytes(b) + if e != nil { + return "", "", e + } + if len(line) == 0 { + return "", "", nil + } + + // Scan first line for colon. + for i := 0; i < len(line); i++ { + switch line[i] { + case ' ': + // Key field has space - no good. + return "", "", BadHeader + case ':': + key = string(line[0:i]); + // Skip initial space before value. + for i++; i < len(line); i++ { + if line[i] != ' ' { + break + } + } + value = string(line[i:len(line)]) + + // Look for extension lines, which must begin with space. + for { + var c byte; + + if c, e = b.ReadByte(); e != nil { + return "", "", e + } + if c != ' ' { + // Not leading space; stop. + b.UnreadByte() + break + } + + // Eat leading space. + for c == ' ' { + if c, e = b.ReadByte(); e != nil { + return "", "", e + } + } + b.UnreadByte() + + // Read the rest of the line and add to value. + if line, e = ReadLineBytes(b); e != nil { + return "", "", e + } + value += " " + string(line) + + if len(value) >= MaxValueLength { + return "", "", ValueTooLong + } + } + return key, value, nil + } + } + + // Line ended before space or colon. + return "", "", BadHeader; +} + +// Convert decimal at s[i:len(s)] to integer, +// returning value, string position where the digits stopped, +// and whether there was a valid number (digits, not too big). +func atoi(s string, i int) (n, i1 int, ok bool) { + const Big = 1000000 + if i >= len(s) || s[i] < '0' || s[i] > '9' { + return 0, 0, false + } + n = 0 + for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') + if n > Big { + return 0, 0, false + } + } + return n, i, true +} + +// Parse HTTP version: "HTTP/1.2" -> (1, 2, true). +func ParseHTTPVersion(vers string) (int, int, bool) { + if vers[0:5] != "HTTP/" { + return 0, 0, false + } + major, i, ok := atoi(vers, 5) + if !ok || i >= len(vers) || vers[i] != '.' { + return 0, 0, false + } + var minor int; + minor, i, ok = atoi(vers, i+1) + if !ok || i != len(vers) { + return 0, 0, false + } + return major, minor, true +} + +// Read and parse a request from b. +export func ReadRequest(b *bufio.BufRead) (req *Request, err *os.Error) { + req = new(Request); + + // First line: GET /index.html HTTP/1.0 + var s string + if s, err = ReadLine(b); err != nil { + return nil, err + } + + var f *[]string + if f = strings.split(s, " "); len(f) != 3 { + return nil, BadRequest + } + req.method, req.rawurl, req.proto = f[0], f[1], f[2] + var ok bool; + if req.pmajor, req.pminor, ok = ParseHTTPVersion(req.proto); !ok { + return nil, BadHTTPVersion + } + + if req.url, err = ParseURL(req.rawurl); err != nil { + return nil, err + } + + // Subsequent lines: Key: value. + nheader := 0; + req.header = new(map[string] string) + for { + var key, value string + if key, value, err = ReadKeyValue(b); err != nil { + return nil, err + } + if key == "" { + break + } + if nheader++; nheader >= MaxHeaderLines { + return nil, HeaderTooLong + } + + // RFC 2616 says that if you send the same header key + // multiple times, it has to be semantically equivalent + // to concatenating the values separated by commas. + oldvalue, present := req.header[key] + if present { + req.header[key] = oldvalue+","+value + } else { + req.header[key] = value + } + } + + // RFC2616: Must treat + // GET /index.html HTTP/1.1 + // Host: www.google.com + // and + // GET http://www.google.com/index.html HTTP/1.1 + // Host: doesntmatter + // the same. In the second case, any Host line is ignored. + if v, have := req.header["Host"]; have && req.url.host == "" { + req.host = v + } + + // RFC2616: Should treat + // Pragma: no-cache + // like + // Cache-control: no-cache + if v, have := req.header["Pragma"]; have && v == "no-cache" { + if cc, havecc := req.header["Cache-control"]; !havecc { + req.header["Cache-control"] = "no-cache" + } + } + + // Determine whether to hang up after sending the reply. + if req.pmajor < 1 || (req.pmajor == 1 && req.pminor < 1) { + req.close = true + } else if v, have := req.header["Connection"]; have { + // TODO: Should split on commas, toss surrounding white space, + // and check each field. + if v == "close" { + req.close = true + } + } + + // Pull out useful fields as a convenience to clients. + if v, have := req.header["Referer"]; have { + req.referer = v + } + if v, have := req.header["User-Agent"]; have { + req.useragent = v + } + + + // TODO: Parse specific header values: + // Accept + // Accept-Encoding + // Accept-Language + // Authorization + // Cache-Control + // Connection + // Date + // Expect + // From + // If-Match + // If-Modified-Since + // If-None-Match + // If-Range + // If-Unmodified-Since + // Max-Forwards + // Proxy-Authorization + // Referer [sic] + // TE (transfer-codings) + // Trailer + // Transfer-Encoding + // Upgrade + // User-Agent + // Via + // Warning + + return req, nil; +} diff --git a/src/lib/http/server.go b/src/lib/http/server.go new file mode 100644 index 00000000000..0c9af6c0a4c --- /dev/null +++ b/src/lib/http/server.go @@ -0,0 +1,65 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Trivial HTTP server + +// TODO: Routines for writing responses. + +package http + +import ( + "io"; + "os"; + "net"; + "http"; + "strings" +) + +// Serve a new connection. +func ServeConnection(fd net.Conn, raddr string, f *(*Conn, *Request)) { + c, err := NewConn(fd) + if err != nil { + return + } + for { + req, err := c.ReadRequest() + if err != nil { + break + } + f(c, req) + if c.close { + break + } + } + c.Close(); +} + +// Web server: already listening on l, call f for each request. +export func Serve(l net.Listener, f *(*Conn, *Request)) *os.Error { + // TODO: Make this unnecessary + s, e := os.Getenv("GOMAXPROCS"); + if n, ok := strings.atoi(s); n < 3 { + print("Warning: $GOMAXPROCS needs to be at least 3.\n"); + } + + for { + rw, raddr, e := l.Accept() + if e != nil { + return e + } + go ServeConnection(rw, raddr, f) + } + panic("not reached") +} + +// Web server: listen on address, call f for each request. +export func ListenAndServe(addr string, f *(*Conn, *Request)) *os.Error { + l, e := net.Listen("tcp", addr) + if e != nil { + return e + } + e = Serve(l, f); + l.Close() + return e +} diff --git a/src/lib/http/triv.go b/src/lib/http/triv.go new file mode 100644 index 00000000000..19485f9359f --- /dev/null +++ b/src/lib/http/triv.go @@ -0,0 +1,30 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "io"; + "bufio"; + "os"; + "net"; + "http" +) + +func Echo(conn *http.Conn, req *http.Request) { + fd := conn.bw; + conn.close = true; + io.WriteString(fd, "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "\r\n"); + io.WriteString(fd, req.method+" "+req.rawurl+" "+req.proto+"\r\n") +} + +func main() { + err := http.ListenAndServe("0.0.0.0:12345", &Echo) + if err != nil { + panic("ListenAndServe: ", err.String()) + } +} + diff --git a/src/lib/http/url.go b/src/lib/http/url.go new file mode 100644 index 00000000000..07470e68cab --- /dev/null +++ b/src/lib/http/url.go @@ -0,0 +1,178 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parse URLs (actually URIs, but that seems overly pedantic). + +package http + +import ( + "os" +) + +export var ( + BadURL = os.NewError("bad url syntax") +) + +func IsHex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func UnHex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +// Unescape %xx into hex. +export func URLUnescape(s string) (string, *os.Error) { + // Count %, check that they're well-formed. + n := 0 + for i := 0; i < len(s); { + if s[i] == '%' { + n++ + if !IsHex(s[i+1]) || !IsHex(s[i+2]) { + return "", BadURL + } + i += 3 + } else { + i++ + } + } + + if n == 0 { + return s, nil + } + + t := new([]byte, len(s)-2*n); + j := 0 + for i := 0; i < len(s); { + if s[i] == '%' { + t[j] = UnHex(s[i+1]) << 4 | UnHex(s[i+2]); + j++ + i += 3 + } else { + t[j] = s[i]; + j++ + i++ + } + } + return string(t), nil +} + +export type URL struct { + raw string; + scheme string; + rawpath string; + authority string; + userinfo string; + host string; + path string; + query string; + fragment string; +} + +// Maybe rawurl is of the form scheme:path. +// (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*) +// If so, return scheme, path; else return "", rawurl. +func GetScheme(rawurl string) (scheme, path string, err *os.Error) { + for i := 0; i < len(rawurl); i++ { + c := rawurl[i]; + switch { + case 'a' <= c && c <= 'z' ||'A' <= c && c <= 'Z': + // do nothing + case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.': + if i == 0 { + return "", rawurl, nil + } + case c == ':': + if i == 0 { + return "", "", BadURL + } + return rawurl[0:i], rawurl[i+1:len(rawurl)], nil + } + } + return "", rawurl, nil +} + +// Maybe s is of the form t c u. +// If so, return t, c u (or t, u if cutc == true). +// If not, return s, "". +func Split(s string, c byte, cutc bool) (string, string) { + for i := 0; i < len(s); i++ { + if s[i] == c { + if cutc { + return s[0:i], s[i+1:len(s)] + } + return s[0:i], s[i:len(s)] + } + } + return s, "" +} + +// Parse rawurl into a URL structure. +export func ParseURL(rawurl string) (url *URL, err *os.Error) { + if rawurl == "" { + return nil, BadURL + } + url = new(URL); + url.raw = rawurl + + // Split off possible leading "http:", "mailto:", etc. + var path string + if url.scheme, path, err = GetScheme(rawurl); err != nil { + return nil, err + } + url.rawpath = path + + // RFC 2396: a relative URI (no scheme) has a ?query, + // but absolute URIs only have query if path begins with / + if url.scheme == "" || len(path) > 0 && path[0] == '/' { + path, url.query = Split(path, '?', true); + if url.query, err = URLUnescape(url.query); err != nil { + return nil, err + } + } + + // Maybe path is //authority/path + if len(path) > 2 && path[0:2] == "//" { + url.authority, path = Split(path[2:len(path)], '/', false) + } + url.userinfo, url.host = Split(url.authority, '@', true); + + // What's left is the path. + // TODO: Canonicalize (remove . and ..)? + if url.path, err = URLUnescape(url.path); err != nil { + return nil, err + } + + return url, nil +} + +// A URL reference is a URL with #frag potentially added. Parse it. +export func ParseURLReference(rawurlref string) (url *URL, err *os.Error) { + // Cut off #frag. + rawurl, frag := Split(rawurlref, '#', true); + if url, err = ParseURL(rawurl); err != nil { + return nil, err + } + if url.fragment, err = URLUnescape(frag); err != nil { + return nil, err + } + return url, nil +} +