diff --git a/gemini.go b/gemini.go new file mode 100644 index 0000000..71281a8 --- /dev/null +++ b/gemini.go @@ -0,0 +1,150 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "log" + "mime" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +// Yoinked from jetforce and go'ified +const ( + STATUS_INPUT = 10 + + STATUS_SUCCESS = 20 + STATUS_SUCCESS_END_OF_SESSION = 21 + + STATUS_REDIRECT_TEMPORARY = 30 + STATUS_REDIRECT_PERMANENT = 31 + + STATUS_TEMPORARY_FAILURE = 40 + STATUS_SERVER_UNAVAILABLE = 41 + STATUS_CGI_ERROR = 42 + STATUS_PROXY_ERROR = 43 + STATUS_SLOW_DOWN = 44 + + STATUS_PERMANENT_FAILURE = 50 + STATUS_NOT_FOUND = 51 + STATUS_GONE = 52 + STATUS_PROXY_REQUEST_REFUSED = 53 + STATUS_BAD_REQUEST = 59 + + STATUS_CLIENT_CERTIFICATE_REQUIRED = 60 + STATUS_TRANSIENT_CERTIFICATE_REQUESTED = 61 + STATUS_AUTHORISED_CERTIFICATE_REQUIRED = 62 + STATUS_CERTIFICATE_NOT_ACCEPTED = 63 + STATUS_FUTURE_CERTIFICATE_REJECTED = 64 + STATUS_EXPIRED_CERTIFICATE_REJECTED = 65 +) + +type Response struct { + Status int + Meta string + Body string +} + +type GeminiConfig struct { + Hostname string + KeyFile string + CertFile string + RootDir string + CGIDir string +} + +func (c *GeminiConfig) String() string { + return fmt.Sprintf("Gemini Config: %v Files:%v CGI:%v", c.Hostname, c.RootDir, c.CGIDir) +} + +func generateFile(selector string) Response { + meta := mime.TypeByExtension(filepath.Ext(selector)) + if meta == "" { + //assume plain UTF-8 text + meta = "text/gemini; charset=utf-8" + } + file, err := os.Open(selector) + if err != nil { + panic("Failed to read file") + } + defer file.Close() + buf, err := ioutil.ReadAll(file) + return Response{STATUS_SUCCESS, meta, string(buf)} +} + +func generateDirectory(path string) Response { + var dirpage string + files, err := ioutil.ReadDir(path) + if err != nil { + log.Println(err) + return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory dirpage", ""} + } + dirpage = "# Directory Contents\r\n" + for _, file := range files { + // Don't list hidden files + if isNotWorldReadable(file) || strings.HasPrefix(file.Name(), ".") { + continue + } + if file.Name() == "index.gmi" || file.Name() == "index.gemini" { + //Found an index file, return that instead + return generateFile(path + file.Name()) + } else { + dirpage += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name()) + } + } + return Response{STATUS_SUCCESS, "text/gemini", dirpage} +} + +func generateCGI(selector *url.URL, c *conn) Response { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) //make this customizable + defer cancel() + + cmd := exec.CommandContext(ctx, selector.Path) + + //build CGI environment + cmd.Env = []string{fmt.Sprintf("GEMINI_URL=%v", selector), + fmt.Sprintf("HOSTNAME=%v", selector.Hostname()), + fmt.Sprintf("PATH_INFO=%v", selector.Path), + fmt.Sprintf("QUERY_STRING=%v", selector.RawQuery), + fmt.Sprintf("REMOTE_ADDR=%v", c.C.RemoteAddr()), + fmt.Sprintf("REMOTE_HOST=%v", c.C.RemoteAddr()), + fmt.Sprintf("SERVER_NAME=%v", selector.Hostname()), + fmt.Sprintf("SERVER_PORT=%v", c.server.Port), + "SERVER_PROTOCOL=GEMINI", + "SERVER_SOFTWARE=secretshop/0.1.0", + } + cmdout, err := cmd.Output() + if ctx.Err() == context.DeadlineExceeded { + log.Printf("CGI %v timed out", selector) + return Response{STATUS_CGI_ERROR, "CGI process timed out", ""} + } + if err != nil { + log.Printf("Error running CGI process %v", selector) + return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} + } + + header, _, err := bufio.NewReader(strings.NewReader(string(cmdout))).ReadLine() + if err != nil { + log.Printf("Error running CGI process %v", selector) + return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} + } + header_s := strings.Fields(string(header)) + //make sure it has a valid status + status, err := strconv.Atoi(header_s[0]) + if err != nil { + log.Printf("Error running CGI process %v", selector) + return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} + } + if status < 0 || status > 69 { + log.Printf("CGI script returned bad status %v", selector) + return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} + } + return Response{status, "", string(cmdout)} +} diff --git a/proto.go b/proto.go deleted file mode 100644 index de4090f..0000000 --- a/proto.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import "fmt" - -// Yoinked from jetforce and go'ified -const ( - STATUS_INPUT = 10 - - STATUS_SUCCESS = 20 - STATUS_SUCCESS_END_OF_SESSION = 21 - - STATUS_REDIRECT_TEMPORARY = 30 - STATUS_REDIRECT_PERMANENT = 31 - - STATUS_TEMPORARY_FAILURE = 40 - STATUS_SERVER_UNAVAILABLE = 41 - STATUS_CGI_ERROR = 42 - STATUS_PROXY_ERROR = 43 - STATUS_SLOW_DOWN = 44 - - STATUS_PERMANENT_FAILURE = 50 - STATUS_NOT_FOUND = 51 - STATUS_GONE = 52 - STATUS_PROXY_REQUEST_REFUSED = 53 - STATUS_BAD_REQUEST = 59 - - STATUS_CLIENT_CERTIFICATE_REQUIRED = 60 - STATUS_TRANSIENT_CERTIFICATE_REQUESTED = 61 - STATUS_AUTHORISED_CERTIFICATE_REQUIRED = 62 - STATUS_CERTIFICATE_NOT_ACCEPTED = 63 - STATUS_FUTURE_CERTIFICATE_REJECTED = 64 - STATUS_EXPIRED_CERTIFICATE_REJECTED = 65 -) - -type Response struct { - Status int - Meta string - Body string -} - -type GeminiConfig struct { - Hostname string - KeyFile string - CertFile string - RootDir string - CGIDir string -} - -func (c *GeminiConfig) String() string { - return fmt.Sprintf("Gemini Config: %v Files:%v CGI:%v", c.Hostname, c.RootDir, c.CGIDir) -} diff --git a/server.go b/server.go index a7557ed..a04933d 100644 --- a/server.go +++ b/server.go @@ -6,7 +6,6 @@ import ( "crypto/rand" "crypto/tls" "fmt" - "io/ioutil" "log" "mime" "net" @@ -40,8 +39,10 @@ var ( type Server struct { Addr string // TCP address to listen on, ":gemini" if empty + Port string // TCP port HostnameToRoot map[string]string //FQDN hostname to root folder + HostnameToCGI map[string]string //FQDN hostname to CGI folder } type conn struct { @@ -60,9 +61,10 @@ func (s *Server) newConn(rwc net.Conn) *conn { } func ListenAndServeTLS(port string, cps []GeminiConfig) error { - server := &Server{Addr: ":" + port, HostnameToRoot: make(map[string]string)} + server := &Server{Addr: ":" + port, Port: port, HostnameToRoot: make(map[string]string), HostnameToCGI: make(map[string]string)} for _, c := range cps { server.HostnameToRoot[c.Hostname] = c.RootDir + server.HostnameToCGI[c.Hostname] = c.CGIDir } return server.ListenAndServeTLS(cps) } @@ -140,14 +142,14 @@ func (c *conn) serve(ctx context.Context) { res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""} } else { req = string(data[:count-2]) - res = c.server.ParseRequest(req) + res = c.server.ParseRequest(req, c) } c.sendResponse(res) log.Printf("%v requested %v; responded with %v %v", c.C.RemoteAddr(), req, res.Status, res.Meta) c.C.Close() } -func (s *Server) ParseRequest(req string) Response { +func (s *Server) ParseRequest(req string, c *conn) Response { u, err := url.Parse(req) if err != nil { return Response{STATUS_BAD_REQUEST, "URL invalid", ""} @@ -187,46 +189,19 @@ func (s *Server) ParseRequest(req string) Response { } default: // it's a file - return generateFile(selector) - } -} - -func generateFile(selector string) Response { - meta := mime.TypeByExtension(filepath.Ext(selector)) - if meta == "" { - //assume plain UTF-8 text - meta = "text/gemini; charset=utf-8" - } - file, err := os.Open(selector) - if err != nil { - panic("Failed to read file") - } - defer file.Close() - buf, err := ioutil.ReadAll(file) - return Response{STATUS_SUCCESS, meta, string(buf)} -} - -func generateDirectory(path string) Response { - var dirpage string - files, err := ioutil.ReadDir(path) - if err != nil { - log.Println(err) - return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory dirpage", ""} - } - dirpage = "# Directory Contents\r\n" - for _, file := range files { - // Don't list hidden files - if isNotWorldReadable(file) || strings.HasPrefix(file.Name(), ".") { - continue + matches, err := filepath.Glob(s.HostnameToCGI[u.Hostname()] + "/*") + if err != nil { + log.Printf("%v: Couldn't search for CGI: %v", u.Hostname(), err) + return Response{STATUS_TEMPORARY_FAILURE, "Error finding file", ""} } - if file.Name() == "index.gmi" || file.Name() == "index.gemini" { - //Found an index file, return that instead - return generateFile(path + file.Name()) + if matches != nil && fi.Mode().Perm()&0111 == 0111 { + //CGI file found + return generateCGI(u, c) } else { - dirpage += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name()) + //Normal file found + return generateFile(selector) } } - return Response{STATUS_SUCCESS, "text/gemini", dirpage} } func (c *conn) sendResponse(r Response) error {