package main import ( "bufio" "bytes" "context" "crypto/md5" "crypto/x509" "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 AccessControl CapsuleAcl Lang string } type CapsuleAcl struct { Identified []string Known []string Trusted []string Whitelist 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, extended_meta string) Response { meta := mime.TypeByExtension(filepath.Ext(selector)) if meta == "" { //assume plain UTF-8 text meta = "text/gemini; charset=utf-8" } meta = meta + "; " + extended_meta 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, extended_meta string) Response { var dirpage string files, err := ioutil.ReadDir(path) if err != nil { log.Printf("ERROR: Error generating directory listing: %v\n", 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(), extended_meta) } else { dirpage += fmt.Sprintf("=> %s %s\r\n", url.PathEscape(file.Name()), file.Name()) } } meta := "text/gemini" if extended_meta != "" { meta = meta + "; " + extended_meta } return Response{STATUS_SUCCESS, meta, 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/" + SECRETSHOP_VERSION, } cmdout, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { log.Printf("WARN: CGI %v timed out\n", selector) return Response{STATUS_CGI_ERROR, "CGI process timed out", ""} } if err != nil { log.Printf("WARN: 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("WARN: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("WARN: Error running CGI process %v", selector) return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} } if status < 0 || status > 69 { log.Printf("WARN: CGI script returned bad status %v", selector) return Response{STATUS_CGI_ERROR, "Error running CGI process", ""} } return Response{status, "", string(cmdout)} } func validCert(capsule GeminiConfig, c *x509.Certificate) int { now := time.Now() if c.NotBefore.After(now) { return STATUS_FUTURE_CERTIFICATE_REJECTED } else if c.NotAfter.Before(now) { return STATUS_EXPIRED_CERTIFICATE_REJECTED } fingerprint := md5.Sum(c.Raw) var buf bytes.Buffer for i, f := range fingerprint { if i > 0 { fmt.Fprintf(&buf, ":") } fmt.Fprintf(&buf, "%02X", f) } b, err := ioutil.ReadFile(capsule.AccessControl.Whitelist) if err != nil { log.Printf("ERROR: Unable to oppen whitelist file %v\n", err) return STATUS_TEMPORARY_FAILURE } s := string(b) //check whether s contains substring text if strings.Contains(s, buf.String()) { return 0 } else { return STATUS_CERTIFICATE_NOT_ACCEPTED } }