1
1
Fork 0
mirror of https://tildegit.org/solderpunk/molly-brown synced 2024-05-11 15:26:03 +02:00
molly-brown/dynamic.go
Solderpunk eb85a6e94c Another big refactor, splitting the Config struct in two.
The split reflects that between variables which can and cannot be
overridden by .molly files, and this greatly simplifies the
processing of said files, getting rid of the need for lots of
ugly temporary variable thrashing.
2023-02-25 11:29:13 +01:00

199 lines
5.6 KiB
Go

package main
import (
"bufio"
"context"
"crypto/tls"
"io"
"log"
"net"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
func handleCGI(config SysConfig, path string, cgiPath string, URL *url.URL, logEntry *LogEntry, 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 {
log.Println("Terminating CGI process " + path + " due to exceeding 10 second runtime limit.")
conn.Write([]byte("42 CGI process timed out!\r\n"))
logEntry.Status = 42
return
}
if err != nil {
log.Println("Error running CGI program " + path + ": " + err.Error())
if err, ok := err.(*exec.ExitError); ok {
log.Println("↳ stderr output: " + string(err.Stderr))
}
conn.Write([]byte("42 CGI error!\r\n"))
logEntry.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 {
log.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"))
logEntry.Status = 42
return
}
logEntry.Status = status
// Write response
conn.Write(response)
}
func handleSCGI(URL *url.URL, scgiPath string, scgiSocket string, config SysConfig, logEntry *LogEntry, conn net.Conn) {
// Connect to socket
socket, err := net.Dial("unix", scgiSocket)
if err != nil {
log.Println("Error connecting to SCGI socket " + scgiSocket + ": " + err.Error())
conn.Write([]byte("42 Error connecting to SCGI service!\r\n"))
logEntry.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
log.Println("Error reading from SCGI socket " + scgiSocket + ": " + err.Error())
conn.Write([]byte("42 Error reading from SCGI service!\r\n"))
logEntry.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"))
logEntry.Status = 42
return
}
logEntry.Status = status
}
// Send to client
conn.Write(buffer[:n])
}
}
func prepareCGIVariables(config SysConfig, 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 SysConfig, 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 SysConfig, 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
}