2020-05-14 18:15:44 -04:00
|
|
|
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
|
2020-05-14 18:20:42 -04:00
|
|
|
|
|
|
|
SECRETSHOP_VERSION = "1.0.0"
|
2020-05-14 18:15:44 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
type Response struct {
|
|
|
|
Status int
|
|
|
|
Meta string
|
|
|
|
Body string
|
|
|
|
}
|
|
|
|
|
|
|
|
type GeminiConfig struct {
|
|
|
|
Hostname string
|
|
|
|
KeyFile string
|
|
|
|
CertFile string
|
|
|
|
RootDir string
|
|
|
|
CGIDir string
|
|
|
|
AccessControl CapsuleAcl
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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",
|
2020-05-14 18:20:42 -04:00
|
|
|
"SERVER_SOFTWARE=secretshop/" + SECRETSHOP_VERSION,
|
2020-05-14 18:15:44 -04:00
|
|
|
}
|
|
|
|
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)}
|
|
|
|
}
|
|
|
|
|
|
|
|
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("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
|
|
|
|
}
|
|
|
|
}
|