package main import ( "bufio" "bytes" "crypto/tls" "flag" "fmt" "io" "log" "net" "net/url" "os" "path" "strings" "github.com/h2non/filetype" "github.com/makeworld-the-better-one/go-gemini" ) func writeHeader(c net.Conn, code int, meta string) { fmt.Fprintf(c, "%d %s\r\n", code, meta) } func respond(c net.Conn, code int) { var meta string switch code { case gemini.StatusTemporaryFailure: meta = "Temporary failure" case gemini.StatusBadRequest: meta = "Bad request" case gemini.StatusNotFound: meta = "Not found" } writeHeader(c, code, meta) } func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexByte(data, '\r'); i >= 0 { // We have a full newline-terminated line. return i + 1, data[0:i], nil } // If we're at EOF, we have a final, non-terminated line. Return it. if atEOF { return len(data), data, nil } // Request more data. return 0, nil, nil } func handleConn(c net.Conn) { defer c.Close() var requestData string scanner := bufio.NewScanner(c) scanner.Split(scanCRLF) if scanner.Scan() { requestData = scanner.Text() } if err := scanner.Err(); err != nil { respond(c, gemini.StatusBadRequest) return } request, err := url.Parse(requestData) if err != nil || request.Scheme != "gemini" || request.Host == "" { respond(c, gemini.StatusBadRequest) return } if request.Path == "" { request.Path = "/" } pathBytes := []byte(request.Path) strippedPath := request.Path if strippedPath[0] == '/' { strippedPath = strippedPath[1:] } for _, serve := range config.Serve { var realPath string if serve.Dir != "" && strings.HasPrefix(request.Path, serve.Dir) { realPath = path.Join(serve.Root, request.Path[len(serve.Dir):]) } else if serve.r != nil && serve.r.Match(pathBytes) { realPath = path.Join(serve.Root, strippedPath) } else { continue } fi, err := os.Stat(realPath) if os.IsNotExist(err) { respond(c, gemini.StatusNotFound) return } else if err != nil { respond(c, gemini.StatusTemporaryFailure) return } if mode := fi.Mode(); mode.IsDir() { _, err := os.Stat(path.Join(realPath, "index.gemini")) if err == nil { realPath = path.Join(realPath, "index.gemini") } else { realPath = path.Join(realPath, "index.gmi") } } fi, err = os.Stat(realPath) if os.IsNotExist(err) { respond(c, gemini.StatusNotFound) return } else if err != nil { respond(c, gemini.StatusTemporaryFailure) return } file, _ := os.Open(realPath) defer file.Close() buf := make([]byte, 261) n, _ := file.Read(buf) mimeType := "text/gemini; charset=utf-8" if !strings.HasSuffix(realPath, ".gmi") && !strings.HasSuffix(realPath, ".gemini") { if strings.HasSuffix(realPath, ".html") && strings.HasSuffix(realPath, ".htm") { mimeType = "text/html; charset=utf-8" } else if strings.HasSuffix(realPath, ".txt") && strings.HasSuffix(realPath, ".text") { mimeType = "text/plain; charset=utf-8" } else { kind, _ := filetype.Match(buf[:n]) if kind != filetype.Unknown { mimeType = kind.MIME.Value } } } writeHeader(c, gemini.StatusSuccess, mimeType) c.Write(buf[:n]) io.Copy(c, file) return } respond(c, gemini.StatusNotFound) } func handleListener(l net.Listener) { for { conn, err := l.Accept() if err != nil { log.Fatal(err) } go handleConn(conn) } } func main() { configFile := flag.String("config", "", "path to configuration file") certFile := flag.String("cert", "", "path to certificate file") keyFile := flag.String("key", "", "path to private key file") flag.Parse() if *configFile == "" { homedir, err := os.UserHomeDir() if err == nil && homedir != "" { *configFile = path.Join(homedir, ".config", "twins", "config.yaml") } } err := readconfig(*configFile) if err != nil && *certFile == "" { log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring twins", *configFile, err) } if *certFile != "" { config.Cert = *certFile } if *keyFile != "" { config.Key = *keyFile } if config.Cert == "" || config.Key == "" { log.Fatal("certificate file and private key must be specified (gemini requires TLS for all connections)") } cert, err := tls.LoadX509KeyPair(config.Cert, config.Key) if err != nil { log.Fatalf("failed to load certificate: %s", err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, } listener, err := tls.Listen("tcp", "localhost:8888", tlsConfig) if err != nil { log.Fatalf("failed to listen on %s: %s", "localhost:8888", err) } handleListener(listener) }