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