go-gemini/handler.go

156 lines
4.1 KiB
Go
Raw Normal View History

2021-02-17 17:26:56 -07:00
package gemini
import (
2021-02-23 20:12:04 -07:00
"bytes"
"context"
"io"
2021-02-17 17:26:56 -07:00
"net/url"
"strings"
2021-02-23 20:12:04 -07:00
"time"
2021-02-17 17:26:56 -07:00
)
// A Handler responds to a Gemini request.
//
// ServeGemini should write the response header and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it is not
// valid to use the ResponseWriter after or concurrently with the completion
2021-02-24 17:00:09 -07:00
// of the ServeGemini call.
2021-02-17 17:26:56 -07:00
//
2021-02-24 17:00:09 -07:00
// The provided context is canceled when the client's connection is closed
// or the ServeGemini method returns.
//
2021-02-17 17:26:56 -07:00
// Handlers should not modify the provided Request.
type Handler interface {
ServeGemini(context.Context, ResponseWriter, *Request)
2021-02-17 17:26:56 -07:00
}
// The HandlerFunc type is an adapter to allow the use of ordinary functions
// as Gemini handlers. If f is a function with the appropriate signature,
// HandlerFunc(f) is a Handler that calls f.
type HandlerFunc func(context.Context, ResponseWriter, *Request)
2021-02-17 17:26:56 -07:00
// ServeGemini calls f(ctx, w, r).
func (f HandlerFunc) ServeGemini(ctx context.Context, w ResponseWriter, r *Request) {
f(ctx, w, r)
2021-02-17 17:26:56 -07:00
}
// StatusHandler returns a request handler that responds to each request
// with the provided status code and meta.
func StatusHandler(status Status, meta string) Handler {
return HandlerFunc(func(ctx context.Context, w ResponseWriter, r *Request) {
w.WriteHeader(status, meta)
})
2021-02-17 17:26:56 -07:00
}
// NotFoundHandler returns a simple request handler that replies to each
// request with a “51 Not found” reply.
func NotFoundHandler() Handler {
return StatusHandler(StatusNotFound, "Not found")
2021-02-17 17:26:56 -07:00
}
// StripPrefix returns a handler that serves Gemini requests by removing the
// given prefix from the request URL's Path (and RawPath if set) and invoking
// the handler h. StripPrefix handles a request for a path that doesn't begin
// with prefix by replying with a Gemini 51 not found error. The prefix must
// match exactly: if the prefix in the request contains escaped characters the
// reply is also a Gemini 51 not found error.
func StripPrefix(prefix string, h Handler) Handler {
if prefix == "" {
return h
}
return HandlerFunc(func(ctx context.Context, w ResponseWriter, r *Request) {
2021-02-17 17:26:56 -07:00
p := strings.TrimPrefix(r.URL.Path, prefix)
rp := strings.TrimPrefix(r.URL.RawPath, prefix)
if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
r2 := new(Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = p
r2.URL.RawPath = rp
h.ServeGemini(ctx, w, r2)
2021-02-17 17:26:56 -07:00
} else {
2021-02-20 14:42:18 -07:00
w.WriteHeader(StatusNotFound, "Not found")
2021-02-17 17:26:56 -07:00
}
})
}
2021-02-23 20:12:04 -07:00
// TimeoutHandler returns a Handler that runs h with the given time limit.
//
// The new Handler calls h.ServeGemini to handle each request, but
// if a call runs for longer than its time limit, the handler responds with a
2021-02-24 06:37:52 -07:00
// 40 Temporary Failure error. After such a timeout, writes by h to
// its ResponseWriter will return context.DeadlineExceeded.
2021-02-23 20:12:04 -07:00
func TimeoutHandler(h Handler, dt time.Duration) Handler {
return &timeoutHandler{
h: h,
dt: dt,
}
}
type timeoutHandler struct {
h Handler
dt time.Duration
}
func (t *timeoutHandler) ServeGemini(ctx context.Context, w ResponseWriter, r *Request) {
2021-02-23 20:12:04 -07:00
ctx, cancel := context.WithTimeout(ctx, t.dt)
defer cancel()
buf := &bytes.Buffer{}
tw := &timeoutWriter{
2021-02-24 17:00:09 -07:00
wr: &contextWriter{
ctx: ctx,
cancel: cancel,
done: ctx.Done(),
wc: nopCloser{buf},
},
}
2021-02-23 20:12:04 -07:00
done := make(chan struct{})
go func() {
t.h.ServeGemini(ctx, tw, r)
2021-02-23 20:12:04 -07:00
close(done)
}()
select {
case <-done:
w.WriteHeader(tw.status, tw.meta)
w.Write(buf.Bytes())
2021-02-23 20:12:04 -07:00
case <-ctx.Done():
w.WriteHeader(StatusTemporaryFailure, "Timeout")
}
}
type timeoutWriter struct {
2021-02-24 17:00:09 -07:00
wr io.Writer
status Status
meta string
mediatype string
wroteHeader bool
}
func (w *timeoutWriter) SetMediaType(mediatype string) {
w.mediatype = mediatype
}
func (w *timeoutWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(StatusSuccess, w.mediatype)
}
2021-02-24 17:00:09 -07:00
return w.wr.Write(b)
}
func (w *timeoutWriter) WriteHeader(status Status, meta string) {
if w.wroteHeader {
return
}
w.status = status
w.meta = meta
w.wroteHeader = true
}
func (w *timeoutWriter) Flush() error {
return nil
}