package main import ( "bufio" "bytes" "crypto/tls" "fmt" "io" "io/ioutil" "log" "mime" "net" "net/http" "net/url" "os" "path/filepath" "github.com/caddyserver/certmagic" ) 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, } // interface type geminiRequest interface { GetURL() *url.URL } // implementation type request struct { url *url.URL } func (r request) GetURL() *url.URL { return r.url } // interface type geminiResponse interface { WriteStatus(code int, meta string) (int, error) Write([]byte) (int, error) } // implementation type response struct { statusSent bool status int meta string connection 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.connection, "%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.connection.Write(b) } // interface 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) } // implementations 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.gemini")) if err == nil && sourceFileStat.Mode().IsRegular() { // if it's a directory, transparently insert the index.gemini check req = filepath.Join(req, "index.gemini") } 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) }) } // handler for general http queries (fallthrough for certmagic) type genericHTTPHandler struct { StaticString string } func (h *genericHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h.StaticString != "" { w.Write([]byte(h.StaticString)) return } w.Write([]byte("This is the default http response for the castor server. Try connecting over the gemini protocol instead.\n")) } func handleConnection(log Logger, conn net.Conn, h geminiHandler) { defer conn.Close() scanner := bufio.NewScanner(conn) if ok := scanner.Scan(); !ok { log.Info(scanner.Err()) } u, err := url.Parse(scanner.Text()) if err != nil { log.Info(err) } w := response{ connection: conn, } r := request{ url: u, } recoveryHandler(h).Handle(&w, r) } func main() { log := NewLogger(&log.Logger{}, true) err := mime.AddExtensionType(".gemini", "text/gemini") if err != nil { log.Info("Could not add text/gemini to mime-type database;", err) } magic := certmagic.NewDefault() myACME := certmagic.NewACMEManager(magic, certmagic.DefaultACME) err = magic.CacheUnmanagedCertificatePEMFile("./cert.pem", "./key.pem", []string{}) if err != nil { log.Info(err) } go func() { err := http.ListenAndServe(":80", myACME.HTTPChallengeHandler(&genericHTTPHandler{})) if err != nil { log.Info(err) } }() listener, err := tls.Listen("tcp", "localhost:1965", magic.TLSConfig()) if err != nil { log.Info(err) return } for { conn, err := listener.Accept() if err != nil { log.Debug(err.Error()) continue } go handleConnection(log, conn, fsGeminiHandler{ root: "./root/", DirectoryListing: true, }) } }