diff --git a/client.go b/client.go index 8c34242..5afebdd 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "net" "strings" "time" @@ -74,12 +75,22 @@ func (c *Client) Do(req *Request) (*Response, error) { conn := tls.Client(netConn, config) // Set connection deadline if c.Timeout != 0 { - conn.SetDeadline(time.Now().Add(c.Timeout)) + err := conn.SetDeadline(time.Now().Add(c.Timeout)) + if err != nil { + return nil, fmt.Errorf( + "failed to set connection deadline: %w", err) + } } // Write the request w := bufio.NewWriter(conn) - req.Write(w) + + err = req.Write(w) + if err != nil { + return nil, fmt.Errorf( + "failed to write request data: %w", err) + } + if err := w.Flush(); err != nil { return nil, err } diff --git a/fs.go b/fs.go index c4cf3d3..6afb917 100644 --- a/fs.go +++ b/fs.go @@ -1,6 +1,7 @@ package gemini import ( + "fmt" "io" "mime" "os" @@ -9,8 +10,13 @@ import ( func init() { // Add Gemini mime types - mime.AddExtensionType(".gmi", "text/gemini") - mime.AddExtensionType(".gemini", "text/gemini") + if err := mime.AddExtensionType(".gmi", "text/gemini"); err != nil { + panic(fmt.Errorf("failed to register .gmi extension mimetype: %w", err)) + } + + if err := mime.AddExtensionType(".gemini", "text/gemini"); err != nil { + panic(fmt.Errorf("failed to register .gemini extension mimetype: %w", err)) + } } // FileServer takes a filesystem and returns a Responder which uses that filesystem. @@ -27,7 +33,7 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) { p := path.Clean(r.URL.Path) f, err := fsh.Open(p) if err != nil { - w.WriteStatus(StatusNotFound) + w.Status(StatusNotFound) return } // Detect mimetype @@ -35,7 +41,7 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) { mimetype := mime.TypeByExtension(ext) w.SetMediaType(mimetype) // Copy file to response writer - io.Copy(w, f) + _, _ = io.Copy(w, f) } // TODO: replace with io/fs.FS when available @@ -66,7 +72,7 @@ func (d Dir) Open(name string) (File, error) { func ServeFile(w *ResponseWriter, fs FS, name string) { f, err := fs.Open(name) if err != nil { - w.WriteStatus(StatusNotFound) + w.Status(StatusNotFound) return } // Detect mimetype @@ -74,7 +80,7 @@ func ServeFile(w *ResponseWriter, fs FS, name string) { mimetype := mime.TypeByExtension(ext) w.SetMediaType(mimetype) // Copy file to response writer - io.Copy(w, f) + _, _ = io.Copy(w, f) } func openFile(p string) (File, error) { diff --git a/mux.go b/mux.go index 81a4ee7..0c565ea 100644 --- a/mux.go +++ b/mux.go @@ -138,14 +138,14 @@ func (mux *ServeMux) Respond(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.WriteHeader(StatusRedirect, u.String()) + w.Header(StatusRedirect, u.String()) return } if path != r.URL.Path { u := *r.URL u.Path = path - w.WriteHeader(StatusRedirect, u.String()) + w.Header(StatusRedirect, u.String()) return } @@ -154,7 +154,7 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) { resp := mux.match(path) if resp == nil { - w.WriteStatus(StatusNotFound) + w.Status(StatusNotFound) return } resp.Respond(w, r) diff --git a/server.go b/server.go index 1f9078a..8d09382 100644 --- a/server.go +++ b/server.go @@ -4,10 +4,10 @@ import ( "bufio" "crypto/tls" "errors" + "fmt" "io" "log" "net" - "strconv" "strings" "time" ) @@ -176,18 +176,20 @@ func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) { func (s *Server) respond(conn net.Conn) { defer conn.Close() if d := s.ReadTimeout; d != 0 { - conn.SetReadDeadline(time.Now().Add(d)) + _ = conn.SetReadDeadline(time.Now().Add(d)) } if d := s.WriteTimeout; d != 0 { - conn.SetWriteDeadline(time.Now().Add(d)) + _ = conn.SetWriteDeadline(time.Now().Add(d)) } w := NewResponseWriter(conn) - defer w.b.Flush() + defer func() { + _ = w.Flush() + }() req, err := ReadRequest(conn) if err != nil { - w.WriteStatus(StatusBadRequest) + w.Status(StatusBadRequest) return } @@ -206,7 +208,7 @@ func (s *Server) respond(conn net.Conn) { resp := s.responder(req) if resp == nil { - w.WriteStatus(StatusNotFound) + w.Status(StatusNotFound) return } @@ -236,6 +238,8 @@ func (s *Server) logf(format string, args ...interface{}) { // ResponseWriter is used by a Gemini handler to construct a Gemini response. type ResponseWriter struct { + status Status + meta string b *bufio.Writer bodyAllowed bool wroteHeader bool @@ -249,34 +253,28 @@ func NewResponseWriter(w io.Writer) *ResponseWriter { } } -// WriteHeader writes the response header. -// If the header has already been written, WriteHeader does nothing. +// Header sets the response header. // // Meta contains more information related to the response status. // For successful responses, Meta should contain the mimetype of the response. // For failure responses, Meta should contain a short description of the failure. // Meta should not be longer than 1024 bytes. -func (w *ResponseWriter) WriteHeader(status Status, meta string) { - if w.wroteHeader { - return - } - w.b.WriteString(strconv.Itoa(int(status))) - w.b.WriteByte(' ') - w.b.WriteString(meta) - w.b.Write(crlf) - - // Only allow body to be written on successful status codes. - if status.Class() == StatusClassSuccess { - w.bodyAllowed = true - } - w.wroteHeader = true +func (w *ResponseWriter) Header(status Status, meta string) { + w.status = status + w.meta = meta } -// WriteStatus writes the response header with the given status code. +// Status sets the response header to the given status code. // -// WriteStatus is equivalent to WriteHeader(status, status.Message()) -func (w *ResponseWriter) WriteStatus(status Status) { - w.WriteHeader(status, status.Message()) +// Status is equivalent to Header(status, status.Message()) +func (w *ResponseWriter) Status(status Status) { + meta := status.Message() + + if status.Class() == StatusClassSuccess { + meta = w.mediatype + } + + w.Header(status, meta) } // SetMediaType sets the media type that will be written for a successful response. @@ -293,20 +291,58 @@ func (w *ResponseWriter) SetMediaType(mediatype string) { // with StatusSuccess and the mimetype set in SetMimetype. func (w *ResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { - mediatype := w.mediatype - if mediatype == "" { - mediatype = "text/gemini" + err := w.writeHeader() + if err != nil { + return 0, err } - w.WriteHeader(StatusSuccess, mediatype) } + if !w.bodyAllowed { return 0, ErrBodyNotAllowed } + return w.b.Write(b) } +func (w *ResponseWriter) writeHeader() error { + status := w.status + if status == 0 { + status = StatusSuccess + } + + meta := w.meta + + if status.Class() == StatusClassSuccess { + w.bodyAllowed = true + + if meta == "" { + meta = w.mediatype + } + + if meta == "" { + meta = "text/gemini" + } + } + + _, err := fmt.Fprintf(w.b, "%d %s\r\n", int(status), meta) + if err != nil { + return fmt.Errorf("failed to write response header: %w", err) + } + + w.wroteHeader = true + + return nil +} + // Flush writes any buffered data to the underlying io.Writer. func (w *ResponseWriter) Flush() error { + if !w.wroteHeader { + err := w.writeHeader() + if err != nil { + return err + } + } + return w.b.Flush() } diff --git a/text.go b/text.go index 9b77982..6263d93 100644 --- a/text.go +++ b/text.go @@ -88,17 +88,17 @@ func (l LineText) line() {} type Text []Line // ParseText parses Gemini text from the provided io.Reader. -func ParseText(r io.Reader) Text { +func ParseText(r io.Reader) (Text, error) { var t Text - ParseLines(r, func(line Line) { + err := ParseLines(r, func(line Line) { t = append(t, line) }) - return t + return t, err } // ParseLines parses Gemini text from the provided io.Reader. // It calls handler with each line that it parses. -func ParseLines(r io.Reader, handler func(Line)) { +func ParseLines(r io.Reader, handler func(Line)) error { const spacetab = " \t" var pre bool scanner := bufio.NewScanner(r) @@ -149,6 +149,8 @@ func ParseLines(r io.Reader, handler func(Line)) { } handler(line) } + + return scanner.Err() } // String writes the Gemini text response to a string and returns it. diff --git a/tofu.go b/tofu.go index 9e93ac2..9a252d6 100644 --- a/tofu.go +++ b/tofu.go @@ -52,12 +52,17 @@ func (k *KnownHostsFile) Lookup(hostname string) (Fingerprint, bool) { } // Write writes a known hosts entry to the configured output. -func (k *KnownHostsFile) Write(hostname string, fingerprint Fingerprint) { +func (k *KnownHostsFile) Write(hostname string, fingerprint Fingerprint) error { k.mu.RLock() defer k.mu.RUnlock() if k.out != nil { - k.writeKnownHost(k.out, hostname, fingerprint) + _, err := k.writeKnownHost(k.out, hostname, fingerprint) + if err != nil { + return fmt.Errorf("failed to write to known host file: %w", err) + } } + + return nil } // WriteAll writes all of the known hosts to the provided io.Writer.