From 039c58c9d00a4a5886fa99d7c7d472e6d02d6a67 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 29 Apr 2023 16:24:38 -0600 Subject: initial spartan server support --- spartan/request.go | 62 ++++++++++++++++++++++++++++++++++ spartan/response.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ spartan/serve.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 spartan/request.go create mode 100644 spartan/response.go create mode 100644 spartan/serve.go (limited to 'spartan') diff --git a/spartan/request.go b/spartan/request.go new file mode 100644 index 0000000..331b68c --- /dev/null +++ b/spartan/request.go @@ -0,0 +1,62 @@ +package spartan + +import ( + "bufio" + "errors" + "io" + "net/url" + "strconv" + "strings" + + "tildegit.org/tjp/gus" +) + +var ( + // InvalidRequestLine indicates a malformed first-line of a spartan request. + InvalidRequestLine = errors.New("invalid request line") + + // InvalidRequestLineEnding says that a spartan request's first line wasn't terminated with CRLF. + InvalidRequestLineEnding = errors.New("invalid request line ending") +) + +// ParseRequest parses a single spartan request and the indicated content-length from a reader. +// +// If ther reader artument is a *bufio.Reader, it will only read a single line from it. +func ParseRequest(rdr io.Reader) (*gus.Request, int, error) { + bufrdr, ok := rdr.(*bufio.Reader) + if !ok { + bufrdr = bufio.NewReader(rdr) + } + + line, err := bufrdr.ReadString('\n') + if err != io.EOF && err != nil { + return nil, 0, err + } + + host, rest, ok := strings.Cut(line, " ") + if !ok { + return nil, 0, InvalidRequestLine + } + path, rest, ok := strings.Cut(line, " ") + if !ok { + return nil, 0, InvalidRequestLine + } + + if len(rest) < 2 || line[len(line)-2:] != "\r\n" { + return nil, 0, InvalidRequestLineEnding + } + + contentlen, err := strconv.Atoi(line[:len(line)-2]) + if err != nil { + return nil, 0, err + } + + return &gus.Request{ + URL: &url.URL{ + Scheme: "spartan", + Host: host, + Path: path, + RawPath: path, + }, + }, contentlen, nil +} diff --git a/spartan/response.go b/spartan/response.go new file mode 100644 index 0000000..edc1db6 --- /dev/null +++ b/spartan/response.go @@ -0,0 +1,96 @@ +package spartan + +import ( + "bytes" + "io" + "sync" + + "tildegit.org/tjp/gus" +) + +// The spartan response types. +const ( + StatusSuccess gus.Status = 2 + StatusRedirect gus.Status = 3 + StatusClientError gus.Status = 4 + StatusServerError gus.Status = 5 +) + +// Success builds a successful spartan response. +func Success(mediatype string, body io.Reader) *gus.Response { + return &gus.Response{ + Status: StatusSuccess, + Meta: mediatype, + Body: body, + } +} + +// Redirect builds a spartan redirect response. +func Redirect(url string) *gus.Response { + return &gus.Response{ + Status: StatusRedirect, + Meta: url, + } +} + +// ClientError builds a "client error" spartan response. +func ClientError(err error) *gus.Response { + return &gus.Response{ + Status: StatusClientError, + Meta: err.Error(), + } +} + +// ServerError builds a "server error" spartan response. +func ServerError(err error) *gus.Response { + return &gus.Response{ + Status: StatusServerError, + Meta: err.Error(), + } +} + +// NewResponseReader builds a reader for a response. +func NewResponseReader(response *gus.Response) gus.ResponseReader { + return &responseReader{ + Response: response, + once: &sync.Once{}, + } +} + +type responseReader struct { + *gus.Response + reader io.Reader + once *sync.Once +} + +func (rdr *responseReader) Read(b []byte) (int, error) { + rdr.ensureReader() + return rdr.reader.Read(b) +} + +func (rdr *responseReader) WriteTo(dst io.Writer) (int64, error) { + rdr.ensureReader() + return rdr.reader.(io.WriterTo).WriteTo(dst) +} + +func (rdr *responseReader) ensureReader() { + rdr.once.Do(func() { + hdr := bytes.NewBuffer(rdr.headerLine()) + if rdr.Body != nil { + rdr.reader = io.MultiReader(hdr, rdr.Body) + } else { + rdr.reader = hdr + } + }) +} + +func (rdr *responseReader) headerLine() []byte { + meta := rdr.Meta.(string) + buf := make([]byte, len(meta)+4) + buf[0] = byte(rdr.Status) + '0' + buf[1] = ' ' + copy(buf[2:], meta) + buf[len(buf)-2] = '\r' + buf[len(buf)-1] = '\n' + return buf +} diff --git a/spartan/serve.go b/spartan/serve.go new file mode 100644 index 0000000..677d76c --- /dev/null +++ b/spartan/serve.go @@ -0,0 +1,95 @@ +package spartan + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "net" + "strings" + + "tildegit.org/tjp/gus" + "tildegit.org/tjp/gus/internal" + "tildegit.org/tjp/gus/logging" +) + +type spartanRequestBodyKey struct{} +type spartanRequestBodyLenKey struct{} + +// SpartanRequestBody is the key set in a handler's context for spartan request bodies. +// +// The corresponding value is a *bufio.Reader from which the request body can be read. +var SpartanRequestBody = spartanRequestBodyKey{} + +// SpartanRequestBodyLen is the key set in a handler's context for the content-length of the request. +// +// The corresponding value is an int. +var SpartanRequestBodyLen = spartanRequestBodyLenKey{} + +type spartanServer struct { + internal.Server + handler gus.Handler +} + +func (ss spartanServer) Protocol() string { return "SPARTAN" } + +// NewServer builds a spartan server. +func NewServer( + ctx context.Context, + hostname string, + network string, + address string, + handler gus.Handler, + errLog logging.Logger, +) (gus.Server, error) { + ss := &spartanServer{handler: handler} + + if strings.IndexByte(hostname, ':') < 0 { + hostname = net.JoinHostPort(hostname, "300") + } + + var err error + ss.Server, err = internal.NewServer(ctx, hostname, network, address, errLog, ss.handleConn) + if err != nil { + return nil, err + } + + return ss, nil +} + +func (ss *spartanServer) handleConn(conn net.Conn) { + buf := bufio.NewReader(conn) + + var response *gus.Response + request, clen, err := ParseRequest(buf) + if err != nil { + response = ClientError(err) + } else { + request.Server = ss + request.RemoteAddr = conn.RemoteAddr() + + var body *bufio.Reader = nil + if clen > 0 { + body = bufio.NewReader(io.LimitReader(buf, int64(clen))) + } + ctx := context.WithValue(ss.Ctx, SpartanRequestBody, body) + ctx = context.WithValue(ctx, SpartanRequestBodyLen, clen) + + defer func() { + if r := recover(); r != nil { + err := fmt.Errorf("%s", r) + _ = ss.LogError("msg", "panic in handler", "err", err) + rdr := NewResponseReader(ServerError(errors.New("Server error"))) + _, _ = io.Copy(conn, rdr) + } + }() + response = ss.handler.Handle(ctx, request) + if response == nil { + response = ClientError(errors.New("Resource does not exist.")) + } + } + + defer response.Close() + _, _ = io.Copy(conn, NewResponseReader(response)) +} -- cgit v1.2.3