summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--spartan/request.go62
-rw-r--r--spartan/response.go96
-rw-r--r--spartan/serve.go95
3 files changed, 253 insertions, 0 deletions
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))
+}