secretshop/src/gemini.go

197 lines
5.3 KiB
Go

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
}
}