From 9e09825537e4ae91119987f979ec4272d1727a2e Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 29 Apr 2023 17:38:26 -0600 Subject: initial spartan client support --- spartan/client.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ spartan/response.go | 36 +++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 spartan/client.go (limited to 'spartan') diff --git a/spartan/client.go b/spartan/client.go new file mode 100644 index 0000000..154b18a --- /dev/null +++ b/spartan/client.go @@ -0,0 +1,70 @@ +package spartan + +import ( + "bytes" + "errors" + "io" + "net" + "strconv" + + "tildegit.org/tjp/gus" +) + +// Client is used for sending spartan requests and receiving responses. +// +// It carries no state and is reusable simultaneously by multiple goroutines. +// +// The zero value is immediately usabble. +type Client struct{} + +// RoundTrip sends a single spartan request and returns its response. +func (c Client) RoundTrip(request *gus.Request, body io.Reader) (*gus.Response, error) { + if request.Scheme != "spartan" && request.Scheme != "" { + return nil, errors.New("non-spartan protocols not supported") + } + + host, port, _ := net.SplitHostPort(request.Host) + if port == "" { + host = request.Host + port = "300" + } + addr := net.JoinHostPort(host, port) + + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + defer conn.Close() + + request.RemoteAddr = conn.RemoteAddr() + + var bodyBytes []byte = nil + if body != nil { + bodyBytes, err = io.ReadAll(body) + if err != nil { + return nil, err + } + } + + requestLine := host + " " + request.EscapedPath() + " " + strconv.Itoa(len(bodyBytes)) + "\r\n" + + if _, err := conn.Write([]byte(requestLine)); err != nil { + return nil, err + } + if _, err := conn.Write(bodyBytes); err != nil { + return nil, err + } + + response, err := ParseResponse(conn) + if err != nil { + return nil, err + } + + bodybuf, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + response.Body = bytes.NewBuffer(bodybuf) + + return response, nil +} diff --git a/spartan/response.go b/spartan/response.go index edc1db6..bd906e0 100644 --- a/spartan/response.go +++ b/spartan/response.go @@ -1,8 +1,11 @@ package spartan import ( + "bufio" "bytes" + "errors" "io" + "strconv" "sync" "tildegit.org/tjp/gus" @@ -37,7 +40,7 @@ func Redirect(url string) *gus.Response { func ClientError(err error) *gus.Response { return &gus.Response{ Status: StatusClientError, - Meta: err.Error(), + Meta: err.Error(), } } @@ -45,10 +48,39 @@ func ClientError(err error) *gus.Response { func ServerError(err error) *gus.Response { return &gus.Response{ Status: StatusServerError, - Meta: err.Error(), + Meta: err.Error(), } } +// InvalidResponseHeaderLine indicates a malformed spartan response line. +var InvalidResponseHeaderLine = errors.New("Invalid response header line.") + +// InvalidResponseLineEnding indicates that a spartan response header didn't end with "\r\n". +var InvalidResponseLineEnding = errors.New("Invalid response line ending.") + +func ParseResponse(rdr io.Reader) (*gus.Response, error) { + bufrdr := bufio.NewReader(rdr) + + hdrLine, err := bufrdr.ReadString('\n') + if err != nil { + return nil, InvalidResponseLineEnding + } + if len(hdrLine) < 2 { + return nil, InvalidResponseHeaderLine + } + + status, err := strconv.Atoi(string(hdrLine[0])) + if err != nil || hdrLine[1] != ' ' || hdrLine[len(hdrLine)-2:] != "\r\n" { + return nil, InvalidResponseHeaderLine + } + + return &gus.Response{ + Status: gus.Status(status), + Meta: hdrLine[2 : len(hdrLine)-2], + Body: bufrdr, + }, nil +} + // NewResponseReader builds a reader for a response. func NewResponseReader(response *gus.Response) gus.ResponseReader { return &responseReader{ -- cgit v1.2.3