diff --git a/handlers/request.go b/handlers/request.go new file mode 100644 index 0000000..fe88423 --- /dev/null +++ b/handlers/request.go @@ -0,0 +1,17 @@ +package handlers + +import "net/url" + +func NewRequest(url *url.URL) *Request { + return &Request{ + url: url, + } +} + +type Request struct { + url *url.URL +} + +func (r *Request) GetURL() *url.URL { + return r.url +} diff --git a/handlers/response.go b/handlers/response.go new file mode 100644 index 0000000..d408f21 --- /dev/null +++ b/handlers/response.go @@ -0,0 +1,173 @@ +package handlers + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "mime" + "net" + "net/http" + "os" + "path/filepath" +) + +var responseCodes = map[string]int{ + "INPUT": 10, + "SENSITIVEINPUT": 11, + "SUCCESS": 20, + "REDIRECTTEMPORARY": 30, + "REDIRECTPERMANENT": 31, + "TEMPORARYFAILURE": 40, + "SERVERUNAVAILABLE": 41, + "CGIERROR": 42, + "PROXYERROR": 43, + "SLOWDOWN": 44, + "PERMANENTFAILURE": 50, + "NOTFOUND": 51, + "GONE": 52, + "PROXYREQUESTREFUSED": 53, + "BADREQUEST": 59, + "CLIENTCERTIFICATEREQUIRED": 60, + "CERTIFICATENOTAUTHORISED": 61, + "CERTIFICATENOTVALID": 62, +} + +func NewResponse(conn net.Conn) *Response { + return &Response{ + conn: conn, + } +} + +type Response struct { + statusSent bool + status int + meta string + conn net.Conn +} + +func (w *Response) WriteStatus(code int, meta string) (int, error) { + if w.statusSent { + return 0, fmt.Errorf("Cannot set status after start of response") + } + w.status = code + w.meta = meta + w.statusSent = true + return fmt.Fprintf(w.conn, "%d %s\r\n", code, meta) +} + +func (w *Response) Write(b []byte) (int, error) { + if !w.statusSent { + // this can't guess text/gemini, of course. + guessedType := http.DetectContentType(b) + w.WriteStatus(responseCodes["SUCCESS"], guessedType) + } + return w.conn.Write(b) +} + +type Handler interface { + Handle(*Response, *Request) +} + +type HandlerFunc func(*Response, *Request) + +// Handle calls f(w, r). +func (f HandlerFunc) Handle(w *Response, r *Request) { + f(w, r) +} + +type StaticHandler struct { + Response string +} + +func (h StaticHandler) Handle(w *Response, r *Request) { + w.WriteStatus(responseCodes["SUCCESS"], "text/gemini") + w.Write([]byte(h.Response)) +} + +func NewFileHandler(root string) *FileHandler { + return &FileHandler{ + Root: root, + DirectoryListing: false, + IndexFile: "index.gmi", + } +} + +type FileHandler struct { + Root string + DirectoryListing bool + IndexFile string +} + +func genIndex(folder, rel string) ([]byte, error) { + files, err := ioutil.ReadDir(folder) + if err != nil { + return []byte{}, err + } + ret := bytes.NewBuffer([]byte{}) + + fmt.Fprintf(ret, "# %s\r\n\r\n", rel) + for _, file := range files { + fmt.Fprintf(ret, "=> %s %s\r\n", filepath.Join(rel, file.Name()), file.Name()) + } + return ret.Bytes(), nil +} + +func (h FileHandler) Handle(w *Response, r *Request) { + // Clean, then join; can't escape the defined root + req := filepath.Join(h.Root, filepath.Clean(r.GetURL().Path)) + + sourceFileStat, err := os.Stat(req) + if err != nil { + w.WriteStatus(responseCodes["NOTFOUND"], "File not found") + return + } + + if sourceFileStat.IsDir() { + sourceFileStat, err = os.Stat(filepath.Join(req, h.IndexFile)) + if err == nil && sourceFileStat.Mode().IsRegular() { + req = filepath.Join(req, h.IndexFile) + } else if h.DirectoryListing { + b, err := genIndex(req, filepath.Clean(r.GetURL().Path)) + if err != nil { + w.WriteStatus(responseCodes["NOTFOUND"], "File not found") + return + } + w.WriteStatus(responseCodes["SUCCESS"], "text/gemini") + w.Write(b) + return + } + } + + if !sourceFileStat.Mode().IsRegular() { + w.WriteStatus(responseCodes["NOTFOUND"], "File not found") + return + } + + source, err := os.Open(req) + if err != nil { + w.WriteStatus(responseCodes["TEMPORARYFAILURE"], "Internal Error") + return + } + defer source.Close() + + mime := mime.TypeByExtension(filepath.Ext(req)) + w.WriteStatus(responseCodes["SUCCESS"], mime) + io.Copy(w, source) +} + +func RecoveryHandler(next Handler) Handler { + return HandlerFunc(func(w *Response, r *Request) { + defer func() { + err := recover() + if err != nil { + log.Println(err) + w.WriteStatus(responseCodes["TEMPORARYFAILURE"], "Internal Error") + return + } + }() + + next.Handle(w, r) + }) +} diff --git a/main.go b/main.go index bf18f7e..73d6772 100644 --- a/main.go +++ b/main.go @@ -2,176 +2,16 @@ package main import ( "bufio" - "bytes" "crypto/tls" - "fmt" - "io" - "io/ioutil" - "log" "mime" "net" - "net/http" "net/url" - "os" - "path/filepath" "git.yetaga.in/alazyreader/castor/certificates" + "git.yetaga.in/alazyreader/castor/handlers" ) -var responseCodes = map[string]int{ - "INPUT": 10, - "SENSITIVEINPUT": 11, - "SUCCESS": 20, - "REDIRECTTEMPORARY": 30, - "REDIRECTPERMANENT": 31, - "TEMPORARYFAILURE": 40, - "SERVERUNAVAILABLE": 41, - "CGIERROR": 42, - "PROXYERROR": 43, - "SLOWDOWN": 44, - "PERMANENTFAILURE": 50, - "NOTFOUND": 51, - "GONE": 52, - "PROXYREQUESTREFUSED": 53, - "BADREQUEST": 59, - "CLIENTCERTIFICATEREQUIRED": 60, - "CERTIFICATENOTAUTHORISED": 61, - "CERTIFICATENOTVALID": 62, -} - -type geminiRequest struct { - url *url.URL -} - -func (r *geminiRequest) GetURL() *url.URL { - return r.url -} - -type geminiResponse struct { - statusSent bool - status int - meta string - connection net.Conn -} - -func (w *geminiResponse) WriteStatus(code int, meta string) (int, error) { - if w.statusSent { - return 0, fmt.Errorf("Cannot set status after start of response") - } - w.status = code - w.meta = meta - w.statusSent = true - return fmt.Fprintf(w.connection, "%d %s\r\n", code, meta) -} - -func (w *geminiResponse) Write(b []byte) (int, error) { - if !w.statusSent { - // this can't guess text/gemini, of course. - guessedType := http.DetectContentType(b) - w.WriteStatus(responseCodes["SUCCESS"], guessedType) - } - return w.connection.Write(b) -} - -type geminiHandler interface { - Handle(*geminiResponse, *geminiRequest) -} - -type geminiHandlerFunc func(*geminiResponse, *geminiRequest) - -// Handle calls f(w, r). -func (f geminiHandlerFunc) Handle(w *geminiResponse, r *geminiRequest) { - f(w, r) -} - -// handlers -type staticGeminiHandler struct { - StaticString string -} - -func (h staticGeminiHandler) Handle(w *geminiResponse, r *geminiRequest) { - w.Write([]byte(h.StaticString)) -} - -type fsGeminiHandler struct { - root string - DirectoryListing bool -} - -func genIndex(folder, rel string) ([]byte, error) { - files, err := ioutil.ReadDir(folder) - if err != nil { - return []byte{}, err - } - ret := bytes.NewBuffer([]byte{}) - - fmt.Fprintf(ret, "# %s\r\n\r\n", rel) - for _, file := range files { - fmt.Fprintf(ret, "=> %s %s\r\n", filepath.Join(rel, file.Name()), file.Name()) - } - return ret.Bytes(), nil -} - -func (h fsGeminiHandler) Handle(w *geminiResponse, r *geminiRequest) { - // Clean, then join; can't escape the defined root - req := filepath.Join(h.root, filepath.Clean(r.GetURL().Path)) - - sourceFileStat, err := os.Stat(req) - if err != nil { - w.WriteStatus(responseCodes["NOTFOUND"], "File not found") - return - } - - if sourceFileStat.IsDir() { - sourceFileStat, err = os.Stat(filepath.Join(req, "index.gmi")) - if err == nil && sourceFileStat.Mode().IsRegular() { - // if it's a directory, transparently insert the index.gmi check - req = filepath.Join(req, "index.gmi") - } else if h.DirectoryListing { - b, err := genIndex(req, filepath.Clean(r.GetURL().Path)) - if err != nil { - w.WriteStatus(responseCodes["NOTFOUND"], "File not found") - return - } - w.WriteStatus(responseCodes["SUCCESS"], "text/gemini") - w.Write(b) - return - } - } - - if !sourceFileStat.Mode().IsRegular() { - w.WriteStatus(responseCodes["NOTFOUND"], "File not found") - return - } - - source, err := os.Open(req) - if err != nil { - w.WriteStatus(responseCodes["TEMPORARYFAILURE"], "Internal Error") - return - } - defer source.Close() - - mime := mime.TypeByExtension(filepath.Ext(req)) - w.WriteStatus(responseCodes["SUCCESS"], mime) - io.Copy(w, source) -} - -func recoveryHandler(next geminiHandler) geminiHandler { - return geminiHandlerFunc(func(w *geminiResponse, r *geminiRequest) { - defer func() { - err := recover() - if err != nil { - log.Println(err) - w.WriteStatus(responseCodes["TEMPORARYFAILURE"], "Internal Error") - return - } - }() - - next.Handle(w, r) - }) -} - -func handleConnection(log Logger, conn net.Conn, h geminiHandler) { +func handleConnection(log Logger, conn net.Conn, h handlers.Handler) { defer conn.Close() scanner := bufio.NewScanner(conn) if ok := scanner.Scan(); !ok { @@ -181,13 +21,9 @@ func handleConnection(log Logger, conn net.Conn, h geminiHandler) { if err != nil { log.Info(err) } - w := &geminiResponse{ - connection: conn, - } - r := &geminiRequest{ - url: u, - } - recoveryHandler(h).Handle(w, r) + w := handlers.NewResponse(conn) + r := handlers.NewRequest(u) + handlers.RecoveryHandler(h).Handle(w, r) } func main() { @@ -236,9 +72,6 @@ func main() { continue } - go handleConnection(log, conn, fsGeminiHandler{ - root: "./root/", - DirectoryListing: true, - }) + go handleConnection(log, conn, handlers.NewFileHandler("./root/")) } }