initial spartan server support
This commit is contained in:
parent
aa6bdb0649
commit
039c58c9d0
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
Reference in New Issue