// Package httpsrv is a small HTTP/1.1 server built directly on net.Listener. // // TinyGo's net/http implements Hijack, but a hijacked connection is closed as // soon as the handler returns and writes to it never reach the peer. A relay // needs a stable upgraded connection, so torly speaks HTTP itself: one request // per connection, Connection: close, and an explicit Hijack that hands the // socket and its buffered reader to the WebSocket layer. package httpsrv import ( "bufio" "errors" "fmt" "io" "net" "strconv" "strings" "sync" "time" ) // Headers maps lowercase header names to their combined values. type Headers map[string]string // Request is one parsed HTTP request. type Request struct { Method string Path string RawQuery string Proto string Header Headers Body []byte RemoteAddr string conn net.Conn brw *bufio.ReadWriter hijacked bool } // Hijack gives the socket and its buffered reader to the caller. After Hijack // the HTTP server writes nothing more on this connection. func (r *Request) Hijack() (net.Conn, *bufio.ReadWriter, error) { if r.hijacked { return nil, nil, errors.New("httpsrv: already hijacked") } r.hijacked = true return r.conn, r.brw, nil } // PathParam is a convenience for routes such as /list/. func (r *Request) PathParam() string { if i := strings.LastIndexByte(r.Path, '/'); i >= 0 { return r.Path[i+1:] } return r.Path } // ResponseWriter writes one response. type ResponseWriter struct { bw *bufio.Writer status int wroteHeader bool headers Headers } // SetHeader sets a response header. func (w *ResponseWriter) SetHeader(name, value string) { if w.headers == nil { w.headers = make(Headers, 6) } w.headers[strings.ToLower(name)] = value } // WriteHeader sends the status line and headers. func (w *ResponseWriter) WriteHeader(status int) { if w.wroteHeader { return } w.status = status w.wroteHeader = true if w.headers == nil { w.headers = make(Headers, 4) } w.headers["connection"] = "close" fmt.Fprintf(w.bw, "HTTP/1.1 %d %s\r\n", status, StatusText(status)) fmt.Fprintf(w.bw, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123)) for k, v := range w.headers { fmt.Fprintf(w.bw, "%s: %s\r\n", canonicalHeader(k), v) } w.bw.WriteString("\r\n") } // Write appends body bytes. func (w *ResponseWriter) Write(p []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(200) } return w.bw.Write(p) } // WriteString appends a string body. func (w *ResponseWriter) WriteString(s string) (int, error) { if !w.wroteHeader { w.WriteHeader(200) } return w.bw.WriteString(s) } // Flush pushes buffered bytes to the socket. func (w *ResponseWriter) Flush() error { return w.bw.Flush() } // Status returns the status code sent, or 0. func (w *ResponseWriter) Status() int { return w.status } func (w *ResponseWriter) finish() error { if !w.wroteHeader { w.WriteHeader(204) } return w.bw.Flush() } func canonicalHeader(k string) string { out := make([]byte, 0, len(k)) upper := true for i := 0; i < len(k); i++ { c := k[i] if upper && c >= 'a' && c <= 'z' { c -= 'a' - 'A' } out = append(out, c) upper = c == '-' } return string(out) } // StatusText returns the reason phrase for common codes. func StatusText(code int) string { switch code { case 200: return "OK" case 201: return "Created" case 202: return "Accepted" case 204: return "No Content" case 304: return "Not Modified" case 400: return "Bad Request" case 401: return "Unauthorized" case 403: return "Forbidden" case 404: return "Not Found" case 405: return "Method Not Allowed" case 409: return "Conflict" case 411: return "Length Required" case 413: return "Payload Too Large" case 415: return "Unsupported Media Type" case 429: return "Too Many Requests" case 500: return "Internal Server Error" case 501: return "Not Implemented" case 503: return "Service Unavailable" default: return "Status" } } // Server accepts connections and dispatches one request per connection. type Server struct { Addr string Handler func(*Request, *ResponseWriter) MaxHeaderBytes int MaxBodyBytes int64 ReadTimeout time.Duration WriteTimeout time.Duration ln net.Listener mu sync.Mutex conns map[net.Conn]struct{} closed bool } // Start binds the listener without serving. It is separate from Serve so a // caller (or a test) can learn the bound port before traffic arrives. func (s *Server) Start() error { if s.MaxHeaderBytes <= 0 { s.MaxHeaderBytes = 32 << 10 } if s.MaxBodyBytes <= 0 { s.MaxBodyBytes = 64 << 20 } ln, err := net.Listen("tcp", s.Addr) if err != nil { return err } s.mu.Lock() s.ln = ln if s.conns == nil { s.conns = make(map[net.Conn]struct{}) } s.mu.Unlock() return nil } // ListenAndServe binds Addr and serves until Close is called. func (s *Server) ListenAndServe() error { if err := s.Start(); err != nil { return err } return s.Serve() } // Serve accepts connections on the bound listener. func (s *Server) Serve() error { s.mu.Lock() ln := s.ln s.mu.Unlock() if ln == nil { return errors.New("httpsrv: not started") } for { c, err := ln.Accept() if err != nil { s.mu.Lock() closed := s.closed s.mu.Unlock() if closed { return nil } return err } sc := &serverConn{Conn: c} s.mu.Lock() if s.closed { s.mu.Unlock() sc.Close() return nil } s.conns[sc] = struct{}{} s.mu.Unlock() go s.serveConn(sc) } } // Addr returns the bound address, useful when Addr used port 0. func (s *Server) BoundAddr() string { s.mu.Lock() defer s.mu.Unlock() if s.ln == nil { return "" } return s.ln.Addr().String() } // Close stops the listener and every live connection. func (s *Server) Close() error { s.mu.Lock() if s.closed { s.mu.Unlock() return nil } s.closed = true ln := s.ln conns := s.conns s.conns = make(map[net.Conn]struct{}) s.mu.Unlock() var err error if ln != nil { err = ln.Close() } for c := range conns { c.Close() } return err } // serverConn makes Close idempotent. TinyGo's TCPConn.Close calls close(2) // unconditionally, so a double close can close an unrelated descriptor that // reused the number. type serverConn struct { net.Conn once sync.Once err error } func (c *serverConn) Close() error { c.once.Do(func() { c.err = c.Conn.Close() }) return c.err } func (s *Server) serveConn(c net.Conn) { defer func() { s.mu.Lock() delete(s.conns, c) s.mu.Unlock() }() if s.ReadTimeout > 0 { c.SetReadDeadline(time.Now().Add(s.ReadTimeout)) } br := bufio.NewReaderSize(c, 8<<10) bw := bufio.NewWriterSize(c, 8<<10) brw := bufio.NewReadWriter(br, bw) rw := &ResponseWriter{bw: bw} req, err := readRequest(c, brw, s.MaxHeaderBytes, s.MaxBodyBytes) if err != nil { if err != errClosed { rw.SetHeader("content-type", "text/plain") rw.WriteHeader(400) rw.WriteString(err.Error() + "\n") rw.finish() } c.Close() return } if req == nil { c.Close() return } if s.WriteTimeout > 0 { c.SetWriteDeadline(time.Now().Add(s.WriteTimeout)) } s.Handler(req, rw) if req.hijacked { return } if err := rw.finish(); err != nil { _ = err } c.Close() } var errClosed = errors.New("closed") func readRequest(c net.Conn, brw *bufio.ReadWriter, maxHeader int, maxBody int64) (*Request, error) { req, err := ReadRequest(brw.Reader, c.RemoteAddr().String(), maxHeader, maxBody, func() error { fmt.Fprintf(brw, "HTTP/1.1 100 Continue\r\n\r\n") return brw.Flush() }) if err != nil || req == nil { return req, err } req.conn = c req.brw = brw return req, nil } // ReadRequest parses one HTTP request from br. It is the transport-neutral // half of the server and is also used by the relay's reader workers, which own // their socket and therefore do not need a net.Conn here. func ReadRequest(br *bufio.Reader, remote string, maxHeader int, maxBody int64, onContinue func() error) (*Request, error) { line, n, err := readLine(br, maxHeader) if err != nil { return nil, err } if line == "" { return nil, nil } headerBytes := n parts := strings.Split(line, " ") if len(parts) != 3 { return nil, errors.New("malformed request line") } req := &Request{ Method: parts[0], Proto: parts[2], Header: make(Headers, 12), RemoteAddr: remote, } target := parts[1] if i := strings.IndexByte(target, '?'); i >= 0 { req.Path = target[:i] req.RawQuery = target[i+1:] } else { req.Path = target } for { line, n, err := readLine(br, maxHeader) if err != nil { return nil, err } headerBytes += n if headerBytes > maxHeader { return nil, errors.New("headers too large") } if line == "" { break } i := strings.IndexByte(line, ':') if i <= 0 { return nil, errors.New("malformed header") } name := strings.ToLower(strings.TrimSpace(line[:i])) value := strings.TrimSpace(line[i+1:]) if old, ok := req.Header[name]; ok && old != "" { req.Header[name] = old + ", " + value } else { req.Header[name] = value } } if te := req.Header["transfer-encoding"]; te != "" && !strings.EqualFold(te, "identity") { return nil, errors.New("chunked requests are not supported") } length := int64(0) if cl := req.Header["content-length"]; cl != "" { v, err := strconv.ParseInt(cl, 10, 64) if err != nil || v < 0 { return nil, errors.New("bad content-length") } length = v } if length > maxBody { return nil, errors.New("request body too large") } if strings.EqualFold(req.Header["expect"], "100-continue") { if onContinue != nil { if err := onContinue(); err != nil { return nil, err } } } if length > 0 { body := make([]byte, length) if _, err := io.ReadFull(br, body); err != nil { return nil, err } req.Body = body } return req, nil } func readLine(br *bufio.Reader, max int) (string, int, error) { line, err := br.ReadString('\n') if err != nil { if err == io.EOF && line == "" { return "", 0, errClosed } if err != io.EOF { return "", 0, err } } if len(line) > max { return "", len(line), errors.New("line too long") } line = strings.TrimSuffix(line, "\n") line = strings.TrimSuffix(line, "\r") return line, len(line) + 1, nil }