introduce responsewriter and interceptor

This commit is contained in:
dre 2021-07-10 01:08:18 +08:00
parent a53418f77c
commit 5714cb2f8b
3 changed files with 123 additions and 22 deletions

View file

@ -70,18 +70,23 @@ type Request struct {
RequestURI string
}
type ResponseWriter interface {
WriteHeader(code int, message string) (int, error)
Write(body []byte) (int, error)
}
type Handler interface {
ServeGemini(io.Writer, *Request)
ServeGemini(ResponseWriter, *Request)
}
// 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(io.Writer, *Request)
type HandlerFunc func(ResponseWriter, *Request)
// ServeGemini calls f(w, r).
func (f HandlerFunc) ServeGemini(w io.Writer, r *Request) {
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
f(w, r)
}
@ -214,12 +219,13 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
<-sem // release
}()
reqChan := make(chan request)
w := &writer{conn}
// push job for which we allocated a sem slot and wait
go requestChannel(conn, reqChan)
select {
case header := <-reqChan:
if header.err != nil {
s.handleRequestError(conn, header)
s.handleRequestError(conn, w, header)
return
}
@ -230,30 +236,41 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
RequestURI: header.rawuri,
RemoteAddr: conn.RemoteAddr().String(),
}
s.Handler.ServeGemini(conn, r)
s.Handler.ServeGemini(w, r)
case <-time.After(s.ReadTimeout):
s.logf("server read timeout, request queue length %v/%v", len(sem), s.MaxOpenConns)
WriteHeader(conn, StatusServerUnavailable, "")
w.WriteHeader(StatusServerUnavailable, "")
}
}
func (s *Server) handleRequestError(conn net.Conn, req request) {
func (s *Server) handleRequestError(conn net.Conn, w ResponseWriter, req request) {
if errors.Is(req.err, ErrEmptyRequest) {
// silently ignore empty requests.
// in debug mode we log these too
s.logf("empty request ignored - %v", conn.RemoteAddr().String())
return
}
s.logf("server error: '%s' %v", strings.TrimSpace(req.rawuri), req.err)
var gmierr *GmiError
if errors.As(req.err, &gmierr) {
WriteHeader(conn, gmierr.Code, gmierr.Error())
// notify if error or redirect
if gmierr.Code == StatusRedirectPermanent || gmierr.Code == StatusRedirectTemporary {
s.logf("redirect '%s' -> '%s' %d - %s",
strings.TrimSpace(req.URL.Path), req.err, gmierr.Code, conn.RemoteAddr().String())
} else {
s.logf("read request error: '%s' %v %d - %s",
strings.TrimSpace(req.rawuri), req.err, gmierr.Code, conn.RemoteAddr().String())
}
w.WriteHeader(gmierr.Code, gmierr.Error())
return
}
// this path doesn't exist currently.
WriteHeader(conn, StatusTemporaryFailure, "internal")
s.logf("unexpected error: '%s' %v - %s",
strings.TrimSpace(req.rawuri), req.err, conn.RemoteAddr().String())
w.WriteHeader(StatusTemporaryFailure, "internal")
}
// conn handler
@ -308,8 +325,11 @@ func readHeader(c net.Conn) (*request, error) {
if parsedURL.Path == "" {
// This error is a redirect path.
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
} else if parsedURL.Path != path.Clean(parsedURL.Path) {
return r, Error(StatusBadRequest, ErrInvalidPath)
} else if cleaned := path.Clean(parsedURL.Path); cleaned != parsedURL.Path {
// check valid alternative if unclean for directories
if cleaned != strings.TrimRight(parsedURL.Path, "/") {
return r, Error(StatusBadRequest, ErrInvalidPath)
}
}
return r, nil
@ -346,17 +366,22 @@ func (s *Server) Shutdown(ctx context.Context) error {
return nil
}
func WriteHeader(c io.Writer, code int, message string) (int, error) {
type writer struct {
w io.Writer
}
func (w *writer) WriteHeader(code int, message string) (int, error) {
// <STATUS><SPACE><META><CR><LF>
if len(message) == 0 {
return c.Write([]byte(fmt.Sprintf("%d%s", code, Termination)))
return w.Write([]byte(fmt.Sprintf("%d%s", code, Termination)))
}
return c.Write([]byte(fmt.Sprintf("%d %s%s", code, message, Termination)))
return w.Write([]byte(fmt.Sprintf("%d %s%s", code, message, Termination)))
}
func Write(c io.Writer, body []byte) (int64, error) {
func (w *writer) Write(body []byte) (int, error) {
reader := bytes.NewReader(body)
return io.Copy(c, reader)
n, err := io.Copy(w.w, reader)
return int(n), err
}

78
gemini/interceptor.go Normal file
View file

@ -0,0 +1,78 @@
package gemini
import (
"bytes"
)
// Interceptor is a ResponseWriter wrapper that may be used as buffer.
//
// A middleware may pass it to the next handlers ServeGemini method as a drop in replacement for the
// response writer. After the ServeGemini method is run the middleware may examine what has been
// written to the Interceptor and decide what to write to the "original" ResponseWriter (that may well be
// another buffer passed from another middleware).
//
// The downside is the body being written two times and the complete caching of the
// body in the memory.
type Interceptor struct {
// ioWriter is the underlying response writer that is wrapped by Interceptor
w ResponseWriter
// Interceptor is the underlying io.Writer that buffers the response body
Body bytes.Buffer
// <STATUS><SPACE><META><CR><LF>
// Code is the status code.
Code int
// Meta is the header addition, such as the mimetype.
Meta string
hasHeader bool
hasBody bool
}
// NewInterceptor creates a new Interceptor by wrapping the given response writer.
func NewInterceptor(w ResponseWriter) (m *Interceptor) {
m = &Interceptor{}
m.w = w
return
}
// WriteHeader writes the cached status code and tracks this call as change
func (m *Interceptor) WriteHeader(code int, message string) (int, error) {
m.hasHeader = true
m.Code = code
m.Meta = message
return 0, nil
}
// Write writes to the underlying buffer and tracks this call as change
func (m *Interceptor) Write(body []byte) (int, error) {
m.hasBody = true
return m.Body.Write(body)
}
func (m *Interceptor) HasHeader() bool {
return m.hasHeader
}
func (m *Interceptor) HasBody() bool {
return m.hasBody
}
// FlushAll flushes headers, status code and body to the underlying ResponseWriter.
func (m *Interceptor) Flush() {
m.FlushHeader()
m.FlushBody()
}
// FlushBody flushes to the underlying responsewriter.
func (m *Interceptor) FlushBody() {
m.w.Write(m.Body.Bytes())
}
// FlushHeader writes the header to the underlying ResponseWriter.
func (m *Interceptor) FlushHeader() {
m.w.WriteHeader(m.Code, m.Meta)
}

View file

@ -1,7 +1,5 @@
package gemini
import "io"
// Middlewares type is a slice of gemini middleware handlers.
type Middleware func(Handler) Handler
@ -24,7 +22,7 @@ func (m *Mux) Handle(endpoint Handler) Handler {
return m.handler
}
func (m *Mux) ServeGemini(w io.Writer, r *Request) {
func (m *Mux) ServeGemini(w ResponseWriter, r *Request) {
m.handler.ServeGemini(w, r)
}