introduce responsewriter and interceptor
This commit is contained in:
parent
a53418f77c
commit
5714cb2f8b
3 changed files with 123 additions and 22 deletions
|
@ -70,18 +70,23 @@ type Request struct {
|
||||||
RequestURI string
|
RequestURI string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResponseWriter interface {
|
||||||
|
WriteHeader(code int, message string) (int, error)
|
||||||
|
Write(body []byte) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
ServeGemini(io.Writer, *Request)
|
ServeGemini(ResponseWriter, *Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The HandlerFunc type is an adapter to allow the use of
|
// The HandlerFunc type is an adapter to allow the use of
|
||||||
// ordinary functions as Gemini handlers. If f is a function
|
// ordinary functions as Gemini handlers. If f is a function
|
||||||
// with the appropriate signature, HandlerFunc(f) is a
|
// with the appropriate signature, HandlerFunc(f) is a
|
||||||
// Handler that calls f.
|
// Handler that calls f.
|
||||||
type HandlerFunc func(io.Writer, *Request)
|
type HandlerFunc func(ResponseWriter, *Request)
|
||||||
|
|
||||||
// ServeGemini calls f(w, r).
|
// 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)
|
f(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,12 +219,13 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
|
||||||
<-sem // release
|
<-sem // release
|
||||||
}()
|
}()
|
||||||
reqChan := make(chan request)
|
reqChan := make(chan request)
|
||||||
|
w := &writer{conn}
|
||||||
// push job for which we allocated a sem slot and wait
|
// push job for which we allocated a sem slot and wait
|
||||||
go requestChannel(conn, reqChan)
|
go requestChannel(conn, reqChan)
|
||||||
select {
|
select {
|
||||||
case header := <-reqChan:
|
case header := <-reqChan:
|
||||||
if header.err != nil {
|
if header.err != nil {
|
||||||
s.handleRequestError(conn, header)
|
s.handleRequestError(conn, w, header)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -230,30 +236,41 @@ func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
|
||||||
RequestURI: header.rawuri,
|
RequestURI: header.rawuri,
|
||||||
RemoteAddr: conn.RemoteAddr().String(),
|
RemoteAddr: conn.RemoteAddr().String(),
|
||||||
}
|
}
|
||||||
s.Handler.ServeGemini(conn, r)
|
s.Handler.ServeGemini(w, r)
|
||||||
|
|
||||||
case <-time.After(s.ReadTimeout):
|
case <-time.After(s.ReadTimeout):
|
||||||
s.logf("server read timeout, request queue length %v/%v", len(sem), s.MaxOpenConns)
|
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) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logf("server error: '%s' %v", strings.TrimSpace(req.rawuri), req.err)
|
|
||||||
|
|
||||||
var gmierr *GmiError
|
var gmierr *GmiError
|
||||||
if errors.As(req.err, &gmierr) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// this path doesn't exist currently.
|
// 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
|
// conn handler
|
||||||
|
@ -308,9 +325,12 @@ func readHeader(c net.Conn) (*request, error) {
|
||||||
if parsedURL.Path == "" {
|
if parsedURL.Path == "" {
|
||||||
// This error is a redirect path.
|
// This error is a redirect path.
|
||||||
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
|
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
|
||||||
} else if parsedURL.Path != path.Clean(parsedURL.Path) {
|
} 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, Error(StatusBadRequest, ErrInvalidPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
@ -346,17 +366,22 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||||
return nil
|
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>
|
// <STATUS><SPACE><META><CR><LF>
|
||||||
if len(message) == 0 {
|
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)
|
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
78
gemini/interceptor.go
Normal 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)
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
// Middlewares type is a slice of gemini middleware handlers.
|
// Middlewares type is a slice of gemini middleware handlers.
|
||||||
type Middleware func(Handler) Handler
|
type Middleware func(Handler) Handler
|
||||||
|
|
||||||
|
@ -24,7 +22,7 @@ func (m *Mux) Handle(endpoint Handler) Handler {
|
||||||
return m.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)
|
m.handler.ServeGemini(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue