summaryrefslogtreecommitdiff
path: root/gemini
diff options
context:
space:
mode:
Diffstat (limited to 'gemini')
-rw-r--r--gemini/client.go4
-rw-r--r--gemini/handler.go54
-rw-r--r--gemini/handler_test.go117
-rw-r--r--gemini/request.go41
-rw-r--r--gemini/request_test.go17
-rw-r--r--gemini/response.go204
-rw-r--r--gemini/response_test.go23
-rw-r--r--gemini/roundtrip_test.go5
-rw-r--r--gemini/serve.go70
9 files changed, 150 insertions, 385 deletions
diff --git a/gemini/client.go b/gemini/client.go
index 0e8dd07..4f99078 100644
--- a/gemini/client.go
+++ b/gemini/client.go
@@ -6,6 +6,8 @@ import (
"errors"
"io"
"net"
+
+ "tildegit.org/tjp/gus"
)
// Client is used for sending gemini requests and parsing gemini responses.
@@ -31,7 +33,7 @@ func NewClient(tlsConf *tls.Config) Client {
//
// This method will not automatically follow redirects or cache permanent failures or
// redirects.
-func (client Client) RoundTrip(request *Request) (*Response, error) {
+func (client Client) RoundTrip(request *gus.Request) (*gus.Response, error) {
if request.Scheme != "gemini" && request.Scheme != "" {
return nil, errors.New("non-gemini protocols not supported")
}
diff --git a/gemini/handler.go b/gemini/handler.go
deleted file mode 100644
index 0f48e62..0000000
--- a/gemini/handler.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package gemini
-
-import "context"
-
-// Handler is a function which can turn a gemini request into a gemini response.
-//
-// A Handler MUST NOT return a nil response. Errors should be returned in the form
-// of error responses (4x, 5x, 6x response status). If the Handler should not be
-// responsible for the requested resource it can return a 51 response.
-type Handler func(context.Context, *Request) *Response
-
-// Middleware is a handle decorator.
-//
-// It returns a handler which may call the passed-in handler or not, or may
-// transform the request or response in some way.
-type Middleware func(Handler) Handler
-
-// FallthroughHandler builds a handler which tries multiple child handlers.
-//
-// The returned handler will invoke each of the passed child handlers in order,
-// stopping when it receives a response with status other than 51.
-func FallthroughHandler(handlers ...Handler) Handler {
- return func(ctx context.Context, req *Request) *Response {
- for _, handler := range handlers {
- response := handler(ctx, req)
- if response.Status != StatusNotFound {
- return response
- }
- }
-
- return NotFound("Resource does not exist.")
- }
-}
-
-// Filter wraps a handler with a predicate which determines whether to run the handler.
-//
-// When the predicate function returns false, the Filter returns the provided failure
-// response. The failure argument may be nil, in which case a "51 Resource does not exist."
-// response will be used.
-func Filter(
- predicate func(context.Context, *Request) bool,
- handler Handler,
- failure *Response,
-) Handler {
- if failure == nil {
- failure = NotFound("Resource does not exist.")
- }
- return func(ctx context.Context, req *Request) *Response {
- if !predicate(ctx, req) {
- return failure
- }
- return handler(ctx, req)
- }
-}
diff --git a/gemini/handler_test.go b/gemini/handler_test.go
deleted file mode 100644
index c83df65..0000000
--- a/gemini/handler_test.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package gemini_test
-
-import (
- "bytes"
- "context"
- "io"
- "net/url"
- "strings"
- "testing"
-
- "tildegit.org/tjp/gus/gemini"
-)
-
-func TestFallthrough(t *testing.T) {
- h1 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
- if req.Path == "/one" {
- return gemini.Success("text/gemini", bytes.NewBufferString("one"))
- }
- return gemini.NotFound("nope")
- }
-
- h2 := func(ctx context.Context, req *gemini.Request) *gemini.Response {
- if req.Path == "/two" {
- return gemini.Success("text/gemini", bytes.NewBufferString("two"))
- }
- return gemini.NotFound("no way")
- }
-
- fth := gemini.FallthroughHandler(h1, h2)
-
- u, err := url.Parse("gemini://test.local/one")
- if err != nil {
- t.Fatalf("url.Parse: %s", err.Error())
- }
-
- resp := fth(context.Background(), &gemini.Request{URL: u})
-
- if resp.Status != gemini.StatusSuccess {
- t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
- }
-
- if resp.Meta != "text/gemini" {
- t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- t.Errorf("Read: %s", err.Error())
- }
- if string(body) != "one" {
- t.Errorf(`expected body "one", got %q`, string(body))
- }
-
- u, err = url.Parse("gemini://test.local/two")
- if err != nil {
- t.Fatalf("url.Parse: %s", err.Error())
- }
-
- resp = fth(context.Background(), &gemini.Request{URL: u})
-
- if resp.Status != gemini.StatusSuccess {
- t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
- }
-
- if resp.Meta != "text/gemini" {
- t.Errorf(`expected meta "text/gemini", got %q`, resp.Meta)
- }
-
- body, err = io.ReadAll(resp.Body)
- if err != nil {
- t.Errorf("Read: %s", err.Error())
- }
- if string(body) != "two" {
- t.Errorf(`expected body "two", got %q`, string(body))
- }
-
- u, err = url.Parse("gemini://test.local/three")
- if err != nil {
- t.Fatalf("url.Parse: %s", err.Error())
- }
-
- resp = fth(context.Background(), &gemini.Request{URL: u})
-
- if resp.Status != gemini.StatusNotFound {
- t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
- }
-}
-
-func TestFilter(t *testing.T) {
- pred := func(ctx context.Context, req *gemini.Request) bool {
- return strings.HasPrefix(req.Path, "/allow")
- }
- base := func(ctx context.Context, req *gemini.Request) *gemini.Response {
- return gemini.Success("text/gemini", bytes.NewBufferString("allowed!"))
- }
- handler := gemini.Filter(pred, base, nil)
-
- u, err := url.Parse("gemini://test.local/allow/please")
- if err != nil {
- t.Fatalf("url.Parse: %s", err.Error())
- }
-
- resp := handler(context.Background(), &gemini.Request{URL: u})
- if resp.Status != gemini.StatusSuccess {
- t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
- }
-
- u, err = url.Parse("gemini://test.local/disallow/please")
- if err != nil {
- t.Fatalf("url.Parse: %s", err.Error())
- }
-
- resp = handler(context.Background(), &gemini.Request{URL: u})
- if resp.Status != gemini.StatusNotFound {
- t.Errorf("expected status %d, got %d", gemini.StatusNotFound, resp.Status)
- }
-}
diff --git a/gemini/request.go b/gemini/request.go
index 933281b..ced7d0b 100644
--- a/gemini/request.go
+++ b/gemini/request.go
@@ -2,43 +2,18 @@ package gemini
import (
"bufio"
- "crypto/tls"
"errors"
"io"
- "net"
"net/url"
+
+ "tildegit.org/tjp/gus"
)
// InvalidRequestLineEnding indicates that a gemini request didn't end with "\r\n".
var InvalidRequestLineEnding = errors.New("invalid request line ending")
-// Request represents a request over the gemini protocol.
-type Request struct {
- // URL is the specific URL being fetched by the request.
- *url.URL
-
- // Server is the server which received the request.
- //
- // This is only populated in gemini servers.
- // It is unused on the client end.
- Server *Server
-
- // RemoteAddr is the address of the other side of the connection.
- //
- // This will be the server address for clients, or the connecting
- // client's address in servers.
- //
- // Be aware though that proxies (and reverse proxies) can confuse this.
- RemoteAddr net.Addr
-
- // TLSState contains information about the TLS encryption over the connection.
- //
- // This includes peer certificates and version information.
- TLSState *tls.ConnectionState
-}
-
// ParseRequest parses a single gemini request from a reader.
-func ParseRequest(rdr io.Reader) (*Request, error) {
+func ParseRequest(rdr io.Reader) (*gus.Request, error) {
line, err := bufio.NewReader(rdr).ReadString('\n')
if err != io.EOF && err != nil {
return nil, err
@@ -57,13 +32,5 @@ func ParseRequest(rdr io.Reader) (*Request, error) {
u.Scheme = "gemini"
}
- return &Request{URL: u}, nil
-}
-
-// UnescapedQuery performs %XX unescaping on the URL query segment.
-//
-// Like URL.Query(), it silently drops malformed %-encoded sequences.
-func (req Request) UnescapedQuery() string {
- unescaped, _ := url.QueryUnescape(req.RawQuery)
- return unescaped
+ return &gus.Request{URL: u}, nil
}
diff --git a/gemini/request_test.go b/gemini/request_test.go
index c23d54b..1da24f7 100644
--- a/gemini/request_test.go
+++ b/gemini/request_test.go
@@ -3,7 +3,6 @@ package gemini_test
import (
"bytes"
"testing"
- "net/url"
"tildegit.org/tjp/gus/gemini"
)
@@ -85,19 +84,3 @@ func TestParseRequest(t *testing.T) {
})
}
}
-
-func TestUnescapedQuery(t *testing.T) {
- table := []string{
- "foo bar",
- }
-
- for _, test := range table {
- t.Run(test, func(t *testing.T) {
- u, _ := url.Parse("gemini://domain.com/path?" + url.QueryEscape(test))
- result := gemini.Request{ URL: u }.UnescapedQuery()
- if result != test {
- t.Errorf("expected %q, got %q", test, result)
- }
- })
- }
-}
diff --git a/gemini/response.go b/gemini/response.go
index 5b5ced4..0452462 100644
--- a/gemini/response.go
+++ b/gemini/response.go
@@ -6,65 +6,68 @@ import (
"errors"
"io"
"strconv"
+
+ "tildegit.org/tjp/gus"
)
-// StatusCategory represents the various types of responses.
-type StatusCategory int
+// ResponseCategory represents the various types of gemini responses.
+type ResponseCategory int
const (
- // StatusCategoryInput is for responses which request additional input.
+ // ResponseCategoryInput is for responses which request additional input.
//
// The META line will be the prompt to display to the user.
- StatusCategoryInput StatusCategory = iota*10 + 10
- // StatusCategorySuccess is for successful responses.
+ ResponseCategoryInput ResponseCategory = iota*10 + 10
+ // ResponseCategorySuccess is for successful responses.
//
// The META line will be the resource's mime type.
// This is the only response status which indicates the presence of a response body,
// and it will contain the resource itself.
- StatusCategorySuccess
- // StatusCategoryRedirect is for responses which direct the client to an alternative URL.
+ ResponseCategorySuccess
+ // ResponseCategoryRedirect is for responses which direct the client to an alternative URL.
//
// The META line will contain the new URL the client should try.
- StatusCategoryRedirect
- // StatusCategoryTemporaryFailure is for responses which indicate a transient server-side failure.
+ ResponseCategoryRedirect
+ // ResponseCategoryTemporaryFailure is for responses which indicate a transient server-side failure.
//
// The META line may contain a line with more information about the error.
- StatusCategoryTemporaryFailure
- // StatusCategoryPermanentFailure is for permanent failure responses.
+ ResponseCategoryTemporaryFailure
+ // ResponseCategoryPermanentFailure is for permanent failure responses.
//
// The META line may contain a line with more information about the error.
- StatusCategoryPermanentFailure
- // StatusCategoryCertificateRequired indicates client certificate related issues.
+ ResponseCategoryPermanentFailure
+ // ResponseCategoryCertificateRequired indicates client certificate related issues.
//
// The META line may contain a line with more information about the error.
- StatusCategoryCertificateRequired
+ ResponseCategoryCertificateRequired
)
-// Status is the integer status code of a gemini response.
-type Status int
+func ResponseCategoryForStatus(status gus.Status) ResponseCategory {
+ return ResponseCategory(status / 10)
+}
const (
// StatusInput indicates a required query parameter at the requested URL.
- StatusInput Status = Status(StatusCategoryInput) + iota
+ StatusInput gus.Status = gus.Status(ResponseCategoryInput) + iota
// StatusSensitiveInput indicates a sensitive query parameter is required.
StatusSensitiveInput
)
const (
// StatusSuccess is a successful response.
- StatusSuccess = Status(StatusCategorySuccess) + iota
+ StatusSuccess = gus.Status(ResponseCategorySuccess) + iota
)
const (
// StatusTemporaryRedirect indicates a temporary redirect to another URL.
- StatusTemporaryRedirect = Status(StatusCategoryRedirect) + iota
+ StatusTemporaryRedirect = gus.Status(ResponseCategoryRedirect) + iota
// StatusPermanentRedirect indicates that the resource should always be requested at the new URL.
StatusPermanentRedirect
)
const (
// StatusTemporaryFailure indicates that the request failed and there is no response body.
- StatusTemporaryFailure = Status(StatusCategoryTemporaryFailure) + iota
+ StatusTemporaryFailure = gus.Status(ResponseCategoryTemporaryFailure) + iota
// StatusServerUnavailable occurs when the server is unavailable due to overload or maintenance.
StatusServerUnavailable
// StatusCGIError is the result of a failure of a CGI script.
@@ -80,7 +83,7 @@ const (
const (
// StatusPermanentFailure is a server failure which should be expected to continue indefinitely.
- StatusPermanentFailure = Status(StatusCategoryPermanentFailure) + iota
+ StatusPermanentFailure = gus.Status(ResponseCategoryPermanentFailure) + iota
// StatusNotFound means the resource doesn't exist but it may in the future.
StatusNotFound
// StatusGone occurs when a resource will not be available any longer.
@@ -88,58 +91,37 @@ const (
// StatusProxyRequestRefused means the server is unwilling to act as a proxy for the resource.
StatusProxyRequestRefused
// StatusBadRequest indicates that the request was malformed somehow.
- StatusBadRequest = Status(StatusCategoryPermanentFailure) + 9
+ StatusBadRequest = gus.Status(ResponseCategoryPermanentFailure) + 9
)
const (
// StatusClientCertificateRequired is returned when a certificate was required but not provided.
- StatusClientCertificateRequired = Status(StatusCategoryCertificateRequired) + iota
+ StatusClientCertificateRequired = gus.Status(ResponseCategoryCertificateRequired) + iota
// StatusCertificateNotAuthorized means the certificate doesn't grant access to the requested resource.
StatusCertificateNotAuthorized
// StatusCertificateNotValid means the provided client certificate is invalid.
StatusCertificateNotValid
)
-// StatusCategory returns the category a specific status belongs to.
-func (s Status) Category() StatusCategory {
- return StatusCategory(s / 10)
-}
-
-// Response contains everything in a gemini protocol response.
-type Response struct {
- // Status is the status code of the response.
- Status Status
-
- // Meta is the status-specific line of additional information.
- Meta string
-
- // Body is the response body, if any.
- //
- // It is not guaranteed to be readable more than once.
- Body io.Reader
-
- reader io.Reader
-}
-
// Input builds an input-prompting response.
-func Input(prompt string) *Response {
- return &Response{
+func Input(prompt string) *gus.Response {
+ return &gus.Response{
Status: StatusInput,
Meta: prompt,
}
}
// SensitiveInput builds a password-prompting response.
-func SensitiveInput(prompt string) *Response {
- return &Response{
+func SensitiveInput(prompt string) *gus.Response {
+ return &gus.Response{
Status: StatusSensitiveInput,
Meta: prompt,
}
}
// Success builds a success response with resource body.
-func Success(mediatype string, body io.Reader) *Response {
- return &Response{
+func Success(mediatype string, body io.Reader) *gus.Response {
+ return &gus.Response{
Status: StatusSuccess,
Meta: mediatype,
Body: body,
@@ -147,120 +129,120 @@ func Success(mediatype string, body io.Reader) *Response {
}
// Redirect builds a redirect response.
-func Redirect(url string) *Response {
- return &Response{
+func Redirect(url string) *gus.Response {
+ return &gus.Response{
Status: StatusTemporaryRedirect,
Meta: url,
}
}
// PermanentRedirect builds a response with a permanent redirect.
-func PermanentRedirect(url string) *Response {
- return &Response{
+func PermanentRedirect(url string) *gus.Response {
+ return &gus.Response{
Status: StatusPermanentRedirect,
Meta: url,
}
}
// Failure builds a temporary failure response from an error.
-func Failure(err error) *Response {
- return &Response{
+func Failure(err error) *gus.Response {
+ return &gus.Response{
Status: StatusTemporaryFailure,
Meta: err.Error(),
}
}
// Unavailable build a "server unavailable" response.
-func Unavailable(msg string) *Response {
- return &Response{
+func Unavailable(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusServerUnavailable,
Meta: msg,
}
}
// CGIError builds a "cgi error" response.
-func CGIError(err string) *Response {
- return &Response{
+func CGIError(err string) *gus.Response {
+ return &gus.Response{
Status: StatusCGIError,
Meta: err,
}
}
// ProxyError builds a proxy error response.
-func ProxyError(msg string) *Response {
- return &Response{
+func ProxyError(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusProxyError,
Meta: msg,
}
}
// SlowDown builds a "slow down" response with the number of seconds until the resource is available.
-func SlowDown(seconds int) *Response {
- return &Response{
+func SlowDown(seconds int) *gus.Response {
+ return &gus.Response{
Status: StatusSlowDown,
Meta: strconv.Itoa(seconds),
}
}
// PermanentFailure builds a "permanent failure" from an error.
-func PermanentFailure(err error) *Response {
- return &Response{
+func PermanentFailure(err error) *gus.Response {
+ return &gus.Response{
Status: StatusPermanentFailure,
Meta: err.Error(),
}
}
// NotFound builds a "resource not found" response.
-func NotFound(msg string) *Response {
- return &Response{
+func NotFound(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusNotFound,
Meta: msg,
}
}
// Gone builds a "resource gone" response.
-func Gone(msg string) *Response {
- return &Response{
+func Gone(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusGone,
Meta: msg,
}
}
// RefuseProxy builds a "proxy request refused" response.
-func RefuseProxy(msg string) *Response {
- return &Response{
+func RefuseProxy(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusProxyRequestRefused,
Meta: msg,
}
}
// BadRequest builds a "bad request" response.
-func BadRequest(msg string) *Response {
- return &Response{
+func BadRequest(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusBadRequest,
Meta: msg,
}
}
// RequireCert builds a "client certificate required" response.
-func RequireCert(msg string) *Response {
- return &Response{
+func RequireCert(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusClientCertificateRequired,
Meta: msg,
}
}
// CertAuthFailure builds a "certificate not authorized" response.
-func CertAuthFailure(msg string) *Response {
- return &Response{
+func CertAuthFailure(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusCertificateNotAuthorized,
Meta: msg,
}
}
// CertInvalid builds a "client certificate not valid" response.
-func CertInvalid(msg string) *Response {
- return &Response{
+func CertInvalid(msg string) *gus.Response {
+ return &gus.Response{
Status: StatusCertificateNotValid,
Meta: msg,
}
@@ -275,7 +257,7 @@ var InvalidResponseHeaderLine = errors.New("Invalid response header line.")
// ParseResponse parses a complete gemini response from a reader.
//
// The reader must contain only one gemini response.
-func ParseResponse(rdr io.Reader) (*Response, error) {
+func ParseResponse(rdr io.Reader) (*gus.Response, error) {
bufrdr := bufio.NewReader(rdr)
hdrLine, err := bufrdr.ReadBytes('\n')
@@ -295,53 +277,57 @@ func ParseResponse(rdr io.Reader) (*Response, error) {
return nil, InvalidResponseHeaderLine
}
- return &Response{
- Status: Status(status),
+ return &gus.Response{
+ Status: gus.Status(status),
Meta: string(hdrLine[3:]),
Body: bufrdr,
}, nil
}
-// Read implements io.Reader for Response.
-func (r *Response) Read(b []byte) (int, error) {
- r.ensureReader()
- return r.reader.Read(b)
+type ResponseReader interface {
+ io.Reader
+ io.WriterTo
+ io.Closer
}
-// WriteTo implements io.WriterTo for Response.
-func (r *Response) WriteTo(dst io.Writer) (int64, error) {
- r.ensureReader()
- return r.reader.(io.WriterTo).WriteTo(dst)
+func NewResponseReader(response *gus.Response) ResponseReader {
+ return &responseReader{ Response: response }
}
-// Close implements io.Closer and ensures the body gets closed.
-func (r *Response) Close() error {
- if r != nil {
- if cl, ok := r.Body.(io.Closer); ok {
- return cl.Close()
- }
- }
- return nil
+type responseReader struct {
+ *gus.Response
+ reader io.Reader
+}
+
+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 (r *Response) ensureReader() {
- if r.reader != nil {
+func (rdr *responseReader) ensureReader() {
+ if rdr.reader != nil {
return
}
- hdr := bytes.NewBuffer(r.headerLine())
- if r.Body != nil {
- r.reader = io.MultiReader(hdr, r.Body)
+ hdr := bytes.NewBuffer(rdr.headerLine())
+ if rdr.Body != nil {
+ rdr.reader = io.MultiReader(hdr, rdr.Body)
} else {
- r.reader = hdr
+ rdr.reader = hdr
}
}
-func (r Response) headerLine() []byte {
- buf := make([]byte, len(r.Meta)+5)
- _ = strconv.AppendInt(buf[:0], int64(r.Status), 10)
+func (rdr responseReader) headerLine() []byte {
+ meta := rdr.Meta.(string)
+ buf := make([]byte, len(meta)+5)
+ _ = strconv.AppendInt(buf[:0], int64(rdr.Status), 10)
buf[2] = ' '
- copy(buf[3:], r.Meta)
+ copy(buf[3:], meta)
buf[len(buf)-2] = '\r'
buf[len(buf)-1] = '\n'
return buf
diff --git a/gemini/response_test.go b/gemini/response_test.go
index 616fac4..9287d71 100644
--- a/gemini/response_test.go
+++ b/gemini/response_test.go
@@ -6,14 +6,15 @@ import (
"io"
"testing"
+ "tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
)
func TestBuildResponses(t *testing.T) {
table := []struct {
name string
- response *gemini.Response
- status gemini.Status
+ response *gus.Response
+ status gus.Status
meta string
body string
}{
@@ -137,7 +138,7 @@ func TestBuildResponses(t *testing.T) {
t.Errorf("expected meta %q, got %q", test.meta, test.response.Meta)
}
- responseBytes, err := io.ReadAll(test.response)
+ responseBytes, err := io.ReadAll(gemini.NewResponseReader(test.response))
if err != nil {
t.Fatalf("error reading response body: %q", err.Error())
}
@@ -153,7 +154,7 @@ func TestBuildResponses(t *testing.T) {
func TestParseResponses(t *testing.T) {
table := []struct {
input string
- status gemini.Status
+ status gus.Status
meta string
body string
err error
@@ -232,7 +233,7 @@ func TestParseResponses(t *testing.T) {
func TestResponseClose(t *testing.T) {
body := &rdCloser{Buffer: bytes.NewBufferString("the body here")}
- resp := &gemini.Response{
+ resp := &gus.Response{
Status: gemini.StatusSuccess,
Meta: "text/gemini",
Body: body,
@@ -246,7 +247,7 @@ func TestResponseClose(t *testing.T) {
t.Error("response body was not closed by response.Close()")
}
- resp = &gemini.Response{
+ resp = &gus.Response{
Status: gemini.StatusInput,
Meta: "give me more",
}
@@ -269,8 +270,8 @@ func (rc *rdCloser) Close() error {
func TestResponseWriteTo(t *testing.T) {
// invariant under test: WriteTo() sends the same bytes as Read()
- clone := func(resp *gemini.Response) *gemini.Response {
- other := &gemini.Response{
+ clone := func(resp *gus.Response) *gus.Response {
+ other := &gus.Response{
Status: resp.Status,
Meta: resp.Meta,
}
@@ -296,7 +297,7 @@ func TestResponseWriteTo(t *testing.T) {
table := []struct {
name string
- response *gemini.Response
+ response *gus.Response
}{
{
name: "simple success",
@@ -316,13 +317,13 @@ func TestResponseWriteTo(t *testing.T) {
r1 := test.response
r2 := clone(test.response)
- rdbuf, err := io.ReadAll(r1)
+ rdbuf, err := io.ReadAll(gemini.NewResponseReader(r1))
if err != nil {
t.Fatalf("response.Read(): %s", err.Error())
}
wtbuf := &bytes.Buffer{}
- if _, err := r2.WriteTo(wtbuf); err != nil {
+ if _, err := gemini.NewResponseReader(r2).WriteTo(wtbuf); err != nil {
t.Fatalf("response.WriteTo(): %s", err.Error())
}
diff --git a/gemini/roundtrip_test.go b/gemini/roundtrip_test.go
index 5dd61f1..326ffbc 100644
--- a/gemini/roundtrip_test.go
+++ b/gemini/roundtrip_test.go
@@ -9,6 +9,7 @@ import (
"net/url"
"testing"
+ "tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
)
@@ -18,7 +19,7 @@ func TestRoundTrip(t *testing.T) {
t.Fatalf("FileTLS(): %s", err.Error())
}
- handler := func(ctx context.Context, req *gemini.Request) *gemini.Response {
+ handler := func(ctx context.Context, req *gus.Request) *gus.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page"))
}
@@ -36,7 +37,7 @@ func TestRoundTrip(t *testing.T) {
}
cli := gemini.NewClient(testClientTLS())
- response, err := cli.RoundTrip(&gemini.Request{URL: u})
+ response, err := cli.RoundTrip(&gus.Request{URL: u})
if err != nil {
t.Fatalf("RoundTrip(): %s", err.Error())
}
diff --git a/gemini/serve.go b/gemini/serve.go
index bc13531..c148558 100644
--- a/gemini/serve.go
+++ b/gemini/serve.go
@@ -6,27 +6,28 @@ import (
"io"
"net"
"sync"
+
+ "tildegit.org/tjp/gus"
)
-// Server listens on a network and serves the gemini protocol.
-type Server struct {
+type server struct {
ctx context.Context
network string
address string
cancel context.CancelFunc
wg *sync.WaitGroup
listener net.Listener
- handler Handler
+ handler gus.Handler
}
-// NewServer builds a server.
+// NewServer builds a gemini server.
func NewServer(
ctx context.Context,
tlsConfig *tls.Config,
network string,
address string,
- handler Handler,
-) (*Server, error) {
+ handler gus.Handler,
+) (gus.Server, error) {
listener, err := net.Listen(network, address)
if err != nil {
return nil, err
@@ -34,7 +35,7 @@ func NewServer(
addr := listener.Addr()
- s := &Server{
+ s := &server{
ctx: ctx,
network: addr.Network(),
address: addr.String(),
@@ -54,7 +55,7 @@ func NewServer(
// It will respect cancellation of the context the server was created with,
// but be aware that Close() must still be called in that case to avoid
// dangling goroutines.
-func (s *Server) Serve() error {
+func (s *server) Serve() error {
s.wg.Add(1)
defer s.wg.Done()
@@ -66,7 +67,7 @@ func (s *Server) Serve() error {
for {
conn, err := s.listener.Accept()
if err != nil {
- if s.closed() {
+ if s.Closed() {
err = nil
}
return err
@@ -77,62 +78,57 @@ func (s *Server) Serve() error {
}
}
-// Close begins a graceful shutdown of the server.
-//
-// It cancels the server's context which interrupts all concurrently running
-// request handlers, if they support it. It then blocks until all resources
-// have been cleaned up and all request handlers have completed.
-func (s *Server) Close() {
+func (s *server) Close() {
s.cancel()
s.wg.Wait()
}
-// Network returns the network type on which the server is running.
-func (s *Server) Network() string {
+func (s *server) Network() string {
return s.network
}
-// Address returns the address on which the server is listening.
-func (s *Server) Address() string {
+func (s *server) Address() string {
return s.address
}
-// Hostname returns just the hostname portion of the listen address.
-func (s *Server) Hostname() string {
+func (s *server) Hostname() string {
host, _, _ := net.SplitHostPort(s.address)
return host
}
-// Port returns the port on which the server is listening.
-func (s *Server) Port() string {
+func (s *server) Port() string {
_, portStr, _ := net.SplitHostPort(s.address)
return portStr
}
-func (s *Server) handleConn(conn net.Conn) {
+func (s *server) handleConn(conn net.Conn) {
defer s.wg.Done()
defer conn.Close()
+ var response *gus.Response
req, err := ParseRequest(conn)
if err != nil {
- _, _ = io.Copy(conn, BadRequest(err.Error()))
+ response = BadRequest(err.Error())
return
- }
+ } else {
+ req.Server = s
+ req.RemoteAddr = conn.RemoteAddr()
+ if tlsconn, ok := conn.(*tls.Conn); req != nil && ok {
+ state := tlsconn.ConnectionState()
+ req.TLSState = &state
+ }
- req.Server = s
- req.RemoteAddr = conn.RemoteAddr()
- if tlsconn, ok := conn.(*tls.Conn); req != nil && ok {
- state := tlsconn.ConnectionState()
- req.TLSState = &state
+ response = s.handler(s.ctx, req)
+ if response == nil {
+ response = NotFound("Resource does not exist.")
+ }
+ defer response.Close()
}
- resp := s.handler(s.ctx, req)
- defer resp.Close()
-
- _, _ = io.Copy(conn, resp)
+ _, _ = io.Copy(conn, NewResponseReader(response))
}
-func (s *Server) propagateCancel() {
+func (s *server) propagateCancel() {
go func() {
defer s.wg.Done()
@@ -141,7 +137,7 @@ func (s *Server) propagateCancel() {
}()
}
-func (s *Server) closed() bool {
+func (s *server) Closed() bool {
select {
case <-s.ctx.Done():
return true