package main import ( "bufio" "context" "crypto/rand" "crypto/tls" "crypto/x509" "fmt" "log" "mime" "net" "net/url" "os" "strings" "unicode/utf8" ) type contextKey struct { name string } func (k *contextKey) String() string { return "gemini context value " + k.name } var ( // ServerContextKey is a context key. It can be used in Gemini // handlers with context.WithValue to access the server that // started the handler. The associated value will be of type *Server. ServerContextKey = &contextKey{"gemini-server"} // LocalAddrContextKey is a context key. It can be used in // Gemini handlers with context.WithValue to access the address // the local address the connection arrived on. // The associated value will be of type net.Addr. LocalAddrContextKey = &contextKey{"local-addr"} ) type Server struct { Addr string // TCP address to listen on, ":gemini" if empty Port string // TCP port HostnameToConfig map[string]GeminiConfig //FQDN hostname to Gemini Config } type conn struct { server *Server C *tls.Conn Cert *x509.Certificate } func (s *Server) newConn(rwc *tls.Conn) *conn { c := &conn{ server: s, C: rwc, } return c } func ListenAndServeTLS(port string, cps []GeminiConfig) error { server := &Server{ Addr: ":" + port, Port: port, HostnameToConfig: make(map[string]GeminiConfig), } for _, c := range cps { server.HostnameToConfig[c.Hostname] = c } return server.ListenAndServeTLS(cps) } func (s *Server) ListenAndServeTLS(configs []GeminiConfig) error { addr := s.Addr mime.AddExtensionType(".gmi", "text/gemini") mime.AddExtensionType(".gemini", "text/gemini") if addr == "" { addr = ":1965" } certs := make([]tls.Certificate, len(configs)) for i, c := range configs { cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) if err != nil { log.Fatalf("FATAL: Error loading certs: %s", err) } certs[i] = cert } config := tls.Config{ Certificates: certs, ClientAuth: tls.RequestClientCert, } config.Rand = rand.Reader ln, err := tls.Listen("tcp", addr, &config) if err != nil { log.Fatalf("FATAL: Server unable to listen: %s", err) } return s.Serve(ln, &config) } func (s *Server) Serve(l net.Listener, conf *tls.Config) error { defer l.Close() ctx := context.Background() ctx = context.WithValue(ctx, ServerContextKey, s) ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr()) for { rw, err := l.Accept() if err != nil { fmt.Errorf("ERROR: Unable to accept client: %s", err) return err } rw_tls := rw.(*tls.Conn) c := s.newConn(rw_tls) go c.serve(ctx) } } func (c *conn) serve(ctx context.Context) { buf := bufio.NewReader(c.C) data := make([]byte, 1026) //1024 for the URL, 2 for the CRLF count := 0 for count < 1026 { if strings.Contains(string(data), "\r\n") { break } b, err := buf.ReadByte() if err != nil { log.Printf("WARN: Couldn't serve request: %v, %v bytes read", err.Error(), count) c.C.Close() return } data[count] = b count = count + 1 } var res Response var req string if len(c.C.ConnectionState().PeerCertificates) > 1 { log.Printf("WARN: Client presented %v certificates, only using first one\n") } if len(c.C.ConnectionState().PeerCertificates) > 0 { c.Cert = c.C.ConnectionState().PeerCertificates[0] } else { c.Cert = nil } if !strings.Contains(string(data), "\r\n") { res = Response{STATUS_BAD_REQUEST, "Request too large", ""} req = "TOO_LONG_REQUEST" } else if !utf8.Valid(data) { res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""} } else { req = string(data[:count-2]) res = c.server.ParseRequest(req, c) } c.sendResponse(res) if c.Cert != nil { log.Printf("INFO: remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), c.Cert.Subject, c.Cert.Issuer) } else { log.Printf("INFO: remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), "Anon", "Anon") } c.C.Close() } func (s *Server) ParseRequest(req string, c *conn) Response { u, err := url.Parse(req) if err != nil { return Response{STATUS_BAD_REQUEST, "URL invalid", ""} } if u.Scheme == "" { u.Scheme = "gemini" } else if u.Scheme != "gemini" { return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Scheme not currently supported", ""} } if u.Port() != "1965" && u.Port() != "" { return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Port not currently supported", ""} } if u.Host == "" { return Response{STATUS_BAD_REQUEST, "Need to specify a host", ""} } else if _, ok := s.HostnameToConfig[u.Hostname()]; !ok { return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Hostname not currently supported", ""} } if strings.Contains(u.Path, "..") { return Response{STATUS_PERMANENT_FAILURE, "Dots in path, assuming bad faith.", ""} } //now check cert stuff capsule := s.HostnameToConfig[u.Hostname()] unescaped_path, err := url.PathUnescape(u.Path) if err != nil { return Response{STATUS_BAD_REQUEST, "URL invalid", ""} } selector := capsule.RootDir + unescaped_path for _, idpath := range capsule.AccessControl.Identified { if strings.Contains(selector, capsule.RootDir+idpath) && c.Cert == nil { return Response{STATUS_CLIENT_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""} } } for _, kpath := range capsule.AccessControl.Known { if strings.Contains(selector, capsule.RootDir+kpath) && c.Cert == nil { return Response{STATUS_TRANSIENT_CERTIFICATE_REQUESTED, "Please provide a client certificate", ""} } } for _, tpath := range capsule.AccessControl.Trusted { match := strings.Contains(selector, capsule.RootDir+tpath) if match && c.Cert == nil { return Response{STATUS_AUTHORISED_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""} } else if match && validCert(capsule, c.Cert) != 0 { return Response{validCert(capsule, c.Cert), "Error", ""} } } fi, err := os.Stat(selector) switch { case err != nil: // File doesn't exist. return Response{STATUS_NOT_FOUND, "Couldn't find file", ""} case os.IsNotExist(err) || os.IsPermission(err): return Response{STATUS_NOT_FOUND, "File does not exist", ""} case isNotWorldReadable(fi): return Response{STATUS_TEMPORARY_FAILURE, "Unable to access file", ""} case fi.IsDir(): if strings.HasSuffix(u.Path, "/") { //build extended meta first ext := "" if capsule.Lang != "" { ext = "lang=" + capsule.Lang } return generateDirectory(selector, ext) } else { return Response{STATUS_REDIRECT_PERMANENT, "gemini://" + u.Hostname() + u.Path + "/", ""} } default: // it's a file match := strings.Contains(selector, capsule.RootDir+capsule.CGIDir) if match && fi.Mode().Perm()&0111 == 0111 { //CGI file found return generateCGI(u, c) } else { //Normal file found //build extended meta first ext := "" if capsule.Lang != "" { ext = "lang=" + capsule.Lang } return generateFile(selector, ext) } } } func (c *conn) sendResponse(r Response) error { c.C.Write([]byte(fmt.Sprintf("%v %v\r\n", r.Status, r.Meta))) if r.Body != "" { c.C.Write([]byte(r.Body)) } return nil } func isNotWorldReadable(file os.FileInfo) bool { return uint64(file.Mode().Perm())&0444 != 0444 }