From 110c2de6de7b0a82b7ce60bb815a02b4bae80bca Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Wed, 17 Feb 2021 13:36:16 -0500 Subject: [PATCH] Redesign ResponseWriter interface --- fs.go | 18 +++++----- mux.go | 6 ++-- response.go | 96 +++++++++++++++++++++++++---------------------------- server.go | 4 +-- 4 files changed, 60 insertions(+), 64 deletions(-) diff --git a/fs.go b/fs.go index 67d9206..52a4f77 100644 --- a/fs.go +++ b/fs.go @@ -49,7 +49,7 @@ func serveContent(w ResponseWriter, name string, content io.Reader) { // Detect mimetype from file extension ext := path.Ext(name) mimetype := mime.TypeByExtension(ext) - w.Meta(mimetype) + w.MediaType(mimetype) io.Copy(w, content) } @@ -80,7 +80,7 @@ func ServeFile(w ResponseWriter, r *Request, fsys fs.FS, name string) { // here and ".." may not be wanted. // Note that name might not contain "..", for example if code (still // incorrectly) used filepath.Join(myDir, r.URL.Path). - w.Header(StatusBadRequest, "invalid URL path") + w.WriteHeader(StatusBadRequest, "invalid URL path") return } serveFile(w, r, fsys, name, false) @@ -105,7 +105,7 @@ func serveFile(w ResponseWriter, r *Request, fsys fs.FS, name string, redirect b // Redirect .../index.gmi to .../ if strings.HasSuffix(r.URL.Path, indexPage) { - w.Header(StatusPermanentRedirect, "./") + w.WriteHeader(StatusPermanentRedirect, "./") return } @@ -117,14 +117,14 @@ func serveFile(w ResponseWriter, r *Request, fsys fs.FS, name string, redirect b f, err := fsys.Open(name) if err != nil { - w.Status(StatusNotFound) + w.WriteHeader(StatusNotFound, "Not found") return } defer f.Close() stat, err := f.Stat() if err != nil { - w.Status(StatusTemporaryFailure) + w.WriteHeader(StatusTemporaryFailure, "Temporary failure") return } @@ -134,13 +134,13 @@ func serveFile(w ResponseWriter, r *Request, fsys fs.FS, name string, redirect b if stat.IsDir() { // Add trailing slash if url[len(url)-1] != '/' { - w.Header(StatusPermanentRedirect, path.Base(url)+"/") + w.WriteHeader(StatusPermanentRedirect, path.Base(url)+"/") return } } else { // Remove trailing slash if url[len(url)-1] == '/' { - w.Header(StatusPermanentRedirect, "../"+path.Base(url)) + w.WriteHeader(StatusPermanentRedirect, "../"+path.Base(url)) return } } @@ -150,7 +150,7 @@ func serveFile(w ResponseWriter, r *Request, fsys fs.FS, name string, redirect b // Redirect if the directory name doesn't end in a slash url := r.URL.Path if url[len(url)-1] != '/' { - w.Header(StatusRedirect, path.Base(url)+"/") + w.WriteHeader(StatusRedirect, path.Base(url)+"/") return } @@ -183,7 +183,7 @@ func dirList(w ResponseWriter, f fs.File) { entries, err = d.ReadDir(-1) } if !ok || err != nil { - w.Header(StatusTemporaryFailure, "Error reading directory") + w.WriteHeader(StatusTemporaryFailure, "Error reading directory") return } diff --git a/mux.go b/mux.go index 80ddaa6..6073ba1 100644 --- a/mux.go +++ b/mux.go @@ -138,14 +138,14 @@ func (mux *ServeMux) ServeGemini(w ResponseWriter, r *Request) { // If the given path is /tree and its handler is not registered, // redirect for /tree/. if u, ok := mux.redirectToPathSlash(path, r.URL); ok { - w.Header(StatusRedirect, u.String()) + w.WriteHeader(StatusRedirect, u.String()) return } if path != r.URL.Path { u := *r.URL u.Path = path - w.Header(StatusRedirect, u.String()) + w.WriteHeader(StatusRedirect, u.String()) return } @@ -154,7 +154,7 @@ func (mux *ServeMux) ServeGemini(w ResponseWriter, r *Request) { resp := mux.match(path) if resp == nil { - w.Status(StatusNotFound) + w.WriteHeader(StatusNotFound, "Not found") return } resp.ServeGemini(w, r) diff --git a/response.go b/response.go index ea2184b..a6f40f5 100644 --- a/response.go +++ b/response.go @@ -7,6 +7,9 @@ import ( "strconv" ) +// The default media type for responses. +const defaultMediaType = "text/gemini; charset=utf-8" + // Response represents the response from a Gemini request. // // The Client returns Responses from servers once the response @@ -73,9 +76,9 @@ func ReadResponse(rc io.ReadCloser) (*Response, error) { if len(meta) > 1024 { return nil, ErrInvalidResponse } - // Default mime type of text/gemini; charset=utf-8 if StatusClass(status) == StatusSuccess && meta == "" { - meta = "text/gemini; charset=utf-8" + // Use default media type + meta = defaultMediaType } resp.Meta = meta @@ -132,30 +135,39 @@ func (b *readCloserBody) Read(p []byte) (n int, err error) { return b.ReadCloser.Read(p) } -// A ResponseWriter interface is used by a Gemini handler -// to construct a Gemini response. +// A ResponseWriter interface is used by a Gemini handler to construct +// a Gemini response. +// +// A ResponseWriter may not be used after the Handler.ServeGemini method +// has returned. type ResponseWriter interface { - // Header sets the response header. - Header(status int, meta string) - - // Status sets the response status code. - // It also sets the response meta to StatusText(status). - Status(status int) - - // Meta sets the response meta. + // MediaType sets the media type that will be sent by Write for a + // successful response. If no media type is set, a default of + // "text/gemini; charset=utf-8" will be used. // - // For successful responses, meta should contain the media type of the response. - // For failure responses, meta should contain a short description of the failure. - // The response meta should not be greater than 1024 bytes. - Meta(meta string) + // Setting the media type after a call to Write or WriteHeader has + // no effect. + MediaType(string) - // Write writes data to the connection as part of the response body. - // If the response status does not allow for a response body, Write returns - // ErrBodyNotAllowed. + // Write writes the data to the connection as part of a Gemini response. // - // Write writes the response header if it has not already been written. - // It writes a successful status code if one is not set. + // If WriteHeader has not yet been called, Write calls WriteHeader with + // StatusSuccess and the media type set in MediaType before writing the data. + // If no media type was set, Write uses a default media type of + // "text/gemini; charset=utf-8". Write([]byte) (int, error) + + // WriteHeader sends a Gemini response header with the provided + // status code and meta. + // + // If WriteHeader is not called explicitly, the first call to Write + // will trigger an implicit call to WriteHeader with a successful + // status code and the media type set in MediaType. + // + // The provided code must be a valid Gemini status code. + // The provided meta must not be longer than 1024 bytes. + // Only one header may be written. + WriteHeader(statusCode int, meta string) } // The Flusher interface is implemented by ResponseWriters that allow a @@ -171,8 +183,7 @@ type Flusher interface { type responseWriter struct { b *bufio.Writer - status int - meta string + mediatype string wroteHeader bool bodyAllowed bool } @@ -188,23 +199,18 @@ func newResponseWriter(w io.Writer) *responseWriter { } } -func (w *responseWriter) Header(status int, meta string) { - w.status = status - w.meta = meta -} - -func (w *responseWriter) Status(status int) { - w.status = status - w.meta = StatusText(status) -} - -func (w *responseWriter) Meta(meta string) { - w.meta = meta +func (w *responseWriter) MediaType(mediatype string) { + w.mediatype = mediatype } func (w *responseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { - w.writeHeader(StatusSuccess) + meta := w.mediatype + if meta == "" { + // Use default media type + meta = defaultMediaType + } + w.WriteHeader(StatusSuccess, meta) } if !w.bodyAllowed { return 0, ErrBodyNotAllowed @@ -212,22 +218,12 @@ func (w *responseWriter) Write(b []byte) (int, error) { return w.b.Write(b) } -func (w *responseWriter) writeHeader(defaultStatus int) { - status := w.status - if status == 0 { - status = defaultStatus - } - - meta := w.meta - if StatusClass(status) == StatusSuccess { +func (w *responseWriter) WriteHeader(statusCode int, meta string) { + if StatusClass(statusCode) == StatusSuccess { w.bodyAllowed = true - - if meta == "" { - meta = "text/gemini" - } } - w.b.WriteString(strconv.Itoa(status)) + w.b.WriteString(strconv.Itoa(statusCode)) w.b.WriteByte(' ') w.b.WriteString(meta) w.b.Write(crlf) @@ -236,7 +232,7 @@ func (w *responseWriter) writeHeader(defaultStatus int) { func (w *responseWriter) Flush() error { if !w.wroteHeader { - w.writeHeader(StatusTemporaryFailure) + w.WriteHeader(StatusTemporaryFailure, "Temporary failure") } // Write errors from writeHeader will be returned here. return w.b.Flush() diff --git a/server.go b/server.go index d5dacd9..a90fe50 100644 --- a/server.go +++ b/server.go @@ -380,7 +380,7 @@ func (srv *Server) respond(conn net.Conn) { req, err := ReadRequest(conn) if err != nil { - w.Status(StatusBadRequest) + w.WriteHeader(StatusBadRequest, "Bad request") w.Flush() return } @@ -396,7 +396,7 @@ func (srv *Server) respond(conn net.Conn) { h := srv.handler(req) if h == nil { - w.Status(StatusNotFound) + w.WriteHeader(StatusNotFound, "Not found") w.Flush() return }