package main import ( "bufio" "context" "crypto/tls" "io" "log" "net" "net/url" "os" "os/exec" "strconv" "strings" "time" ) func handleCGI(config Config, path string, cgiPath string, URL *url.URL, log *LogEntry, errorLog *log.Logger, conn net.Conn) { // Find the shortest leading part of path which maps to an executable file. // Call this part scriptPath, and everything after it pathInfo. components := strings.Split(path, "/") scriptPath := "" pathInfo := "" matched := false for i := 0; i <= len(components); i++ { scriptPath = strings.Join(components[0:i], "/") pathInfo = strings.Join(components[i:], "/") if !strings.HasPrefix(scriptPath, cgiPath) { continue } info, err := os.Stat(scriptPath) if err != nil { break } else if info.IsDir() { continue } else if info.Mode().Perm()&0555 == 0555 { matched = true break } } // If we didn't find a match, give up and let this request be handled as // if it were a static file if !matched { return } // Prepare environment variables vars := prepareCGIVariables(config, URL, conn, scriptPath, pathInfo) // Spawn process ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, scriptPath) cmd.Env = []string{} for key, value := range vars { cmd.Env = append(cmd.Env, key+"="+value) } response, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { errorLog.Println("Terminating CGI process " + path + " due to exceeding 10 second runtime limit.") conn.Write([]byte("42 CGI process timed out!\r\n")) log.Status = 42 return } if err != nil { errorLog.Println("Error running CGI program " + path + ": " + err.Error()) if err, ok := err.(*exec.ExitError); ok { errorLog.Println("↳ stderr output: " + string(err.Stderr)) } conn.Write([]byte("42 CGI error!\r\n")) log.Status = 42 return } // Extract response header header, _, err := bufio.NewReader(strings.NewReader(string(response))).ReadLine() status, err2 := strconv.Atoi(strings.Fields(string(header))[0]) if err != nil || err2 != nil { errorLog.Println("Unable to parse first line of output from CGI process " + path + " as valid Gemini response header. Line was: " + string(header)) conn.Write([]byte("42 CGI error!\r\n")) log.Status = 42 return } log.Status = status // Write response conn.Write(response) } func handleSCGI(URL *url.URL, scgiPath string, scgiSocket string, config Config, log *LogEntry, errorLog *log.Logger, conn net.Conn) { // Connect to socket socket, err := net.Dial("unix", scgiSocket) if err != nil { errorLog.Println("Error connecting to SCGI socket " + scgiSocket + ": " + err.Error()) conn.Write([]byte("42 Error connecting to SCGI service!\r\n")) log.Status = 42 return } defer socket.Close() // Send variables vars := prepareSCGIVariables(config, URL, scgiPath, conn) length := 0 for key, value := range vars { length += len(key) length += len(value) length += 2 } socket.Write([]byte(strconv.Itoa(length) + ":")) for key, value := range vars { socket.Write([]byte(key + "\x00")) socket.Write([]byte(value + "\x00")) } socket.Write([]byte(",")) // Read and relay response buffer := make([]byte, 1027) first := true for { n, err := socket.Read(buffer) if err != nil { if err == io.EOF { break } else if !first { // Err errorLog.Println("Error reading from SCGI socket " + scgiSocket + ": " + err.Error()) conn.Write([]byte("42 Error reading from SCGI service!\r\n")) log.Status = 42 return } else { break } } // Extract status code from first line if first { first = false lines := strings.SplitN(string(buffer), "\r\n", 2) status, err := strconv.Atoi(strings.Fields(lines[0])[0]) if err != nil { conn.Write([]byte("42 CGI error!\r\n")) log.Status = 42 return } log.Status = status } // Send to client conn.Write(buffer[:n]) } } func prepareCGIVariables(config Config, URL *url.URL, conn net.Conn, script_path string, path_info string) map[string]string { vars := prepareGatewayVariables(config, URL, conn) vars["GATEWAY_INTERFACE"] = "CGI/1.1" vars["SCRIPT_PATH"] = script_path vars["PATH_INFO"] = path_info return vars } func prepareSCGIVariables(config Config, URL *url.URL, scgiPath string, conn net.Conn) map[string]string { vars := prepareGatewayVariables(config, URL, conn) vars["SCGI"] = "1" vars["CONTENT_LENGTH"] = "0" vars["SCRIPT_PATH"] = scgiPath vars["PATH_INFO"] = URL.Path[len(scgiPath):] return vars } func prepareGatewayVariables(config Config, URL *url.URL, conn net.Conn) map[string]string { vars := make(map[string]string) vars["QUERY_STRING"] = URL.RawQuery vars["REQUEST_METHOD"] = "" vars["SERVER_NAME"] = config.Hostname vars["SERVER_PORT"] = strconv.Itoa(config.Port) vars["SERVER_PROTOCOL"] = "GEMINI" vars["SERVER_SOFTWARE"] = "MOLLY_BROWN" host, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) vars["REMOTE_ADDR"] = host // Add TLS variables var tlsConn (*tls.Conn) = conn.(*tls.Conn) connState := tlsConn.ConnectionState() // vars["TLS_CIPHER"] = CipherSuiteName(connState.CipherSuite) // Add client cert variables clientCerts := connState.PeerCertificates if len(clientCerts) > 0 { cert := clientCerts[0] vars["TLS_CLIENT_HASH"] = getCertFingerprint(cert) vars["TLS_CLIENT_ISSUER"] = cert.Issuer.String() vars["TLS_CLIENT_ISSUER_CN"] = cert.Issuer.CommonName vars["TLS_CLIENT_SUBJECT"] = cert.Subject.String() vars["TLS_CLIENT_SUBJECT_CN"] = cert.Subject.CommonName // To make it easier to detect when a cert is present vars["AUTH_TYPE"] = "Certificate" } return vars }