]> Cypherpunks repositories - gostls13.git/commitdiff
early HTTP library and trivial server
authorRuss Cox <rsc@golang.org>
Wed, 24 Sep 2008 22:26:55 +0000 (15:26 -0700)
committerRuss Cox <rsc@golang.org>
Wed, 24 Sep 2008 22:26:55 +0000 (15:26 -0700)
R=r
OCL=15777
CL=15791

src/lib/http/Makefile [new file with mode: 0644]
src/lib/http/conn.go [new file with mode: 0644]
src/lib/http/request.go [new file with mode: 0644]
src/lib/http/server.go [new file with mode: 0644]
src/lib/http/triv.go [new file with mode: 0644]
src/lib/http/url.go [new file with mode: 0644]

diff --git a/src/lib/http/Makefile b/src/lib/http/Makefile
new file mode 100644 (file)
index 0000000..12153c4
--- /dev/null
@@ -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 (file)
index 0000000..ad1b7cc
--- /dev/null
@@ -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 (file)
index 0000000..bed911e
--- /dev/null
@@ -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 (file)
index 0000000..0c9af6c
--- /dev/null
@@ -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 (file)
index 0000000..19485f9
--- /dev/null
@@ -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 (file)
index 0000000..07470e6
--- /dev/null
@@ -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
+}
+