Add missing error handling

Error handling is currently missing is a couple of places. Most of
them are i/o related.

This change adds checks, an therefore sometimes also has to change
function signatures by adding an error return value. In the case of
the response writer the status and meta handling is changed and this
also breaks the API.

In some places where we don't have any reasonable I've added
assignment to a blank identifier to make it clear that we're ignoring
an error.

text: read the Err() that can be set by the scanner.

client: check if conn.SetDeadline() returns an error.

client: check if req.Write() returns an error.

fs: panic if mime type registration fails.

server: stop performing i/o in Header/Status functions

By deferring the actual header write to the first Write() or Flush()
call we don't have to do any error handling in Header() or Status().

As Server.respond() now defers a ResponseWriter.Flush() instead of
directly flushing the underlying bufio.Writer this has the added
benefit of ensuring that we always write a header
to the client, even if the responder is a complete NOOP.

tofu: return an error if we fail to write to the known hosts writer.
This commit is contained in:
Hugo Wetterberg 2021-01-07 23:08:50 +01:00 committed by Adnan Maolood
parent efef44c2f9
commit f2921a396f
6 changed files with 107 additions and 47 deletions

View File

@ -6,6 +6,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt"
"net" "net"
"strings" "strings"
"time" "time"
@ -74,12 +75,22 @@ func (c *Client) Do(req *Request) (*Response, error) {
conn := tls.Client(netConn, config) conn := tls.Client(netConn, config)
// Set connection deadline // Set connection deadline
if c.Timeout != 0 { 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 // Write the request
w := bufio.NewWriter(conn) 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 { if err := w.Flush(); err != nil {
return nil, err return nil, err
} }

18
fs.go
View File

@ -1,6 +1,7 @@
package gemini package gemini
import ( import (
"fmt"
"io" "io"
"mime" "mime"
"os" "os"
@ -9,8 +10,13 @@ import (
func init() { func init() {
// Add Gemini mime types // Add Gemini mime types
mime.AddExtensionType(".gmi", "text/gemini") if err := mime.AddExtensionType(".gmi", "text/gemini"); err != nil {
mime.AddExtensionType(".gemini", "text/gemini") 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. // 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) p := path.Clean(r.URL.Path)
f, err := fsh.Open(p) f, err := fsh.Open(p)
if err != nil { if err != nil {
w.WriteStatus(StatusNotFound) w.Status(StatusNotFound)
return return
} }
// Detect mimetype // Detect mimetype
@ -35,7 +41,7 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) {
mimetype := mime.TypeByExtension(ext) mimetype := mime.TypeByExtension(ext)
w.SetMediaType(mimetype) w.SetMediaType(mimetype)
// Copy file to response writer // Copy file to response writer
io.Copy(w, f) _, _ = io.Copy(w, f)
} }
// TODO: replace with io/fs.FS when available // 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) { func ServeFile(w *ResponseWriter, fs FS, name string) {
f, err := fs.Open(name) f, err := fs.Open(name)
if err != nil { if err != nil {
w.WriteStatus(StatusNotFound) w.Status(StatusNotFound)
return return
} }
// Detect mimetype // Detect mimetype
@ -74,7 +80,7 @@ func ServeFile(w *ResponseWriter, fs FS, name string) {
mimetype := mime.TypeByExtension(ext) mimetype := mime.TypeByExtension(ext)
w.SetMediaType(mimetype) w.SetMediaType(mimetype)
// Copy file to response writer // Copy file to response writer
io.Copy(w, f) _, _ = io.Copy(w, f)
} }
func openFile(p string) (File, error) { func openFile(p string) (File, error) {

6
mux.go
View File

@ -138,14 +138,14 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
// If the given path is /tree and its handler is not registered, // If the given path is /tree and its handler is not registered,
// redirect for /tree/. // redirect for /tree/.
if u, ok := mux.redirectToPathSlash(path, r.URL); ok { if u, ok := mux.redirectToPathSlash(path, r.URL); ok {
w.WriteHeader(StatusRedirect, u.String()) w.Header(StatusRedirect, u.String())
return return
} }
if path != r.URL.Path { if path != r.URL.Path {
u := *r.URL u := *r.URL
u.Path = path u.Path = path
w.WriteHeader(StatusRedirect, u.String()) w.Header(StatusRedirect, u.String())
return return
} }
@ -154,7 +154,7 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
resp := mux.match(path) resp := mux.match(path)
if resp == nil { if resp == nil {
w.WriteStatus(StatusNotFound) w.Status(StatusNotFound)
return return
} }
resp.Respond(w, r) resp.Respond(w, r)

View File

@ -4,10 +4,10 @@ import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"net" "net"
"strconv"
"strings" "strings"
"time" "time"
) )
@ -176,18 +176,20 @@ func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
func (s *Server) respond(conn net.Conn) { func (s *Server) respond(conn net.Conn) {
defer conn.Close() defer conn.Close()
if d := s.ReadTimeout; d != 0 { if d := s.ReadTimeout; d != 0 {
conn.SetReadDeadline(time.Now().Add(d)) _ = conn.SetReadDeadline(time.Now().Add(d))
} }
if d := s.WriteTimeout; d != 0 { if d := s.WriteTimeout; d != 0 {
conn.SetWriteDeadline(time.Now().Add(d)) _ = conn.SetWriteDeadline(time.Now().Add(d))
} }
w := NewResponseWriter(conn) w := NewResponseWriter(conn)
defer w.b.Flush() defer func() {
_ = w.Flush()
}()
req, err := ReadRequest(conn) req, err := ReadRequest(conn)
if err != nil { if err != nil {
w.WriteStatus(StatusBadRequest) w.Status(StatusBadRequest)
return return
} }
@ -206,7 +208,7 @@ func (s *Server) respond(conn net.Conn) {
resp := s.responder(req) resp := s.responder(req)
if resp == nil { if resp == nil {
w.WriteStatus(StatusNotFound) w.Status(StatusNotFound)
return 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. // ResponseWriter is used by a Gemini handler to construct a Gemini response.
type ResponseWriter struct { type ResponseWriter struct {
status Status
meta string
b *bufio.Writer b *bufio.Writer
bodyAllowed bool bodyAllowed bool
wroteHeader bool wroteHeader bool
@ -249,34 +253,28 @@ func NewResponseWriter(w io.Writer) *ResponseWriter {
} }
} }
// WriteHeader writes the response header. // Header sets the response header.
// If the header has already been written, WriteHeader does nothing.
// //
// Meta contains more information related to the response status. // Meta contains more information related to the response status.
// For successful responses, Meta should contain the mimetype of the response. // For successful responses, Meta should contain the mimetype of the response.
// For failure responses, Meta should contain a short description of the failure. // For failure responses, Meta should contain a short description of the failure.
// Meta should not be longer than 1024 bytes. // Meta should not be longer than 1024 bytes.
func (w *ResponseWriter) WriteHeader(status Status, meta string) { func (w *ResponseWriter) Header(status Status, meta string) {
if w.wroteHeader { w.status = status
return w.meta = meta
}
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
} }
// 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()) // Status is equivalent to Header(status, status.Message())
func (w *ResponseWriter) WriteStatus(status Status) { func (w *ResponseWriter) Status(status Status) {
w.WriteHeader(status, status.Message()) 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. // 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. // with StatusSuccess and the mimetype set in SetMimetype.
func (w *ResponseWriter) Write(b []byte) (int, error) { func (w *ResponseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader { if !w.wroteHeader {
mediatype := w.mediatype err := w.writeHeader()
if mediatype == "" { if err != nil {
mediatype = "text/gemini" return 0, err
} }
w.WriteHeader(StatusSuccess, mediatype)
} }
if !w.bodyAllowed { if !w.bodyAllowed {
return 0, ErrBodyNotAllowed return 0, ErrBodyNotAllowed
} }
return w.b.Write(b) 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. // Flush writes any buffered data to the underlying io.Writer.
func (w *ResponseWriter) Flush() error { func (w *ResponseWriter) Flush() error {
if !w.wroteHeader {
err := w.writeHeader()
if err != nil {
return err
}
}
return w.b.Flush() return w.b.Flush()
} }

10
text.go
View File

@ -88,17 +88,17 @@ func (l LineText) line() {}
type Text []Line type Text []Line
// ParseText parses Gemini text from the provided io.Reader. // 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 var t Text
ParseLines(r, func(line Line) { err := ParseLines(r, func(line Line) {
t = append(t, line) t = append(t, line)
}) })
return t return t, err
} }
// ParseLines parses Gemini text from the provided io.Reader. // ParseLines parses Gemini text from the provided io.Reader.
// It calls handler with each line that it parses. // 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" const spacetab = " \t"
var pre bool var pre bool
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
@ -149,6 +149,8 @@ func ParseLines(r io.Reader, handler func(Line)) {
} }
handler(line) handler(line)
} }
return scanner.Err()
} }
// String writes the Gemini text response to a string and returns it. // String writes the Gemini text response to a string and returns it.

View File

@ -52,12 +52,17 @@ func (k *KnownHostsFile) Lookup(hostname string) (Fingerprint, bool) {
} }
// Write writes a known hosts entry to the configured output. // 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() k.mu.RLock()
defer k.mu.RUnlock() defer k.mu.RUnlock()
if k.out != nil { 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. // WriteAll writes all of the known hosts to the provided io.Writer.