251 lines
6.8 KiB
Go
251 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log"
|
|
"mime"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type contextKey struct {
|
|
name string
|
|
}
|
|
|
|
func (k *contextKey) String() string {
|
|
return "gemini context value " + k.name
|
|
}
|
|
|
|
var (
|
|
// ServerContextKey is a context key. It can be used in Gemini
|
|
// handlers with context.WithValue to access the server that
|
|
// started the handler. The associated value will be of type *Server.
|
|
ServerContextKey = &contextKey{"gemini-server"}
|
|
|
|
// LocalAddrContextKey is a context key. It can be used in
|
|
// Gemini handlers with context.WithValue to access the address
|
|
// the local address the connection arrived on.
|
|
// The associated value will be of type net.Addr.
|
|
LocalAddrContextKey = &contextKey{"local-addr"}
|
|
)
|
|
|
|
type Server struct {
|
|
Addr string // TCP address to listen on, ":gemini" if empty
|
|
Port string // TCP port
|
|
|
|
HostnameToConfig map[string]GeminiConfig //FQDN hostname to Gemini Config
|
|
}
|
|
|
|
type conn struct {
|
|
server *Server
|
|
C *tls.Conn
|
|
Cert *x509.Certificate
|
|
}
|
|
|
|
func (s *Server) newConn(rwc *tls.Conn) *conn {
|
|
c := &conn{
|
|
server: s,
|
|
C: rwc,
|
|
}
|
|
return c
|
|
}
|
|
|
|
func ListenAndServeTLS(port string, cps []GeminiConfig) error {
|
|
server := &Server{
|
|
Addr: ":" + port,
|
|
Port: port,
|
|
HostnameToConfig: make(map[string]GeminiConfig),
|
|
}
|
|
for _, c := range cps {
|
|
server.HostnameToConfig[c.Hostname] = c
|
|
}
|
|
return server.ListenAndServeTLS(cps)
|
|
}
|
|
|
|
func (s *Server) ListenAndServeTLS(configs []GeminiConfig) error {
|
|
addr := s.Addr
|
|
mime.AddExtensionType(".gmi", "text/gemini")
|
|
mime.AddExtensionType(".gemini", "text/gemini")
|
|
if addr == "" {
|
|
addr = ":1965"
|
|
}
|
|
certs := make([]tls.Certificate, len(configs))
|
|
for i, c := range configs {
|
|
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
|
|
if err != nil {
|
|
log.Fatalf("Error loading certs: %s", err)
|
|
}
|
|
certs[i] = cert
|
|
}
|
|
|
|
config := tls.Config{
|
|
Certificates: certs,
|
|
ClientAuth: tls.RequestClientCert,
|
|
}
|
|
config.Rand = rand.Reader
|
|
|
|
ln, err := tls.Listen("tcp", addr, &config)
|
|
if err != nil {
|
|
log.Fatalf("server: listen: %s", err)
|
|
}
|
|
|
|
return s.Serve(ln, &config)
|
|
}
|
|
|
|
func (s *Server) Serve(l net.Listener, conf *tls.Config) error {
|
|
defer l.Close()
|
|
|
|
ctx := context.Background()
|
|
ctx = context.WithValue(ctx, ServerContextKey, s)
|
|
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
|
|
|
|
for {
|
|
rw, err := l.Accept()
|
|
if err != nil {
|
|
fmt.Errorf("error accepting new client: %s", err)
|
|
return err
|
|
}
|
|
|
|
rw_tls := rw.(*tls.Conn)
|
|
c := s.newConn(rw_tls)
|
|
go c.serve(ctx)
|
|
}
|
|
}
|
|
|
|
func (c *conn) serve(ctx context.Context) {
|
|
buf := bufio.NewReader(c.C)
|
|
data := make([]byte, 1026) //1024 for the URL, 2 for the CRLF
|
|
count := 0
|
|
for count < 1026 {
|
|
if strings.Contains(string(data), "\r\n") {
|
|
break
|
|
}
|
|
b, err := buf.ReadByte()
|
|
if err != nil {
|
|
log.Printf("WARN: Couldn't serve request: %v, %v bytes read", err.Error(), count)
|
|
c.C.Close()
|
|
return
|
|
}
|
|
data[count] = b
|
|
count = count + 1
|
|
}
|
|
var res Response
|
|
var req string
|
|
if len(c.C.ConnectionState().PeerCertificates) > 1 {
|
|
log.Printf("WARN: Client presented %v certificates, only using first one\n")
|
|
}
|
|
if len(c.C.ConnectionState().PeerCertificates) > 0 {
|
|
c.Cert = c.C.ConnectionState().PeerCertificates[0]
|
|
} else {
|
|
c.Cert = nil
|
|
}
|
|
if !strings.Contains(string(data), "\r\n") {
|
|
res = Response{STATUS_BAD_REQUEST, "Request too large", ""}
|
|
req = "TOO_LONG_REQUEST"
|
|
} else if !utf8.Valid(data) {
|
|
res = Response{STATUS_BAD_REQUEST, "URL contains non UTF8 charcaters", ""}
|
|
} else {
|
|
req = string(data[:count-2])
|
|
res = c.server.ParseRequest(req, c)
|
|
}
|
|
c.sendResponse(res)
|
|
if c.Cert != nil {
|
|
log.Printf("remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), c.Cert.Subject, c.Cert.Issuer)
|
|
} else {
|
|
log.Printf("remote=%v status=%v request='%v' bytes=%v subject='%v' issuer='%v'", c.C.RemoteAddr(), res.Status, req, len(res.Body), "Anon", "Anon")
|
|
}
|
|
c.C.Close()
|
|
}
|
|
|
|
func (s *Server) ParseRequest(req string, c *conn) Response {
|
|
u, err := url.Parse(req)
|
|
if err != nil {
|
|
return Response{STATUS_BAD_REQUEST, "URL invalid", ""}
|
|
}
|
|
if u.Scheme == "" {
|
|
u.Scheme = "gemini"
|
|
} else if u.Scheme != "gemini" {
|
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Scheme not currently supported", ""}
|
|
}
|
|
if u.Port() != "1965" && u.Port() != "" {
|
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Port not currently supported", ""}
|
|
}
|
|
if u.Host == "" {
|
|
return Response{STATUS_BAD_REQUEST, "Need to specify a host", ""}
|
|
} else if _, ok := s.HostnameToConfig[u.Hostname()]; !ok {
|
|
return Response{STATUS_PROXY_REQUEST_REFUSED, "Proxying by Hostname not currently supported", ""}
|
|
}
|
|
if strings.Contains(u.Path, "..") {
|
|
return Response{STATUS_PERMANENT_FAILURE, "Dots in path, assuming bad faith.", ""}
|
|
}
|
|
|
|
//now check cert stuff
|
|
capsule := s.HostnameToConfig[u.Hostname()]
|
|
selector := capsule.RootDir + u.Path
|
|
for _, idpath := range capsule.AccessControl.Identified {
|
|
if strings.Contains(selector, capsule.RootDir+idpath) && c.Cert == nil {
|
|
return Response{STATUS_CLIENT_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""}
|
|
}
|
|
}
|
|
for _, kpath := range capsule.AccessControl.Known {
|
|
if strings.Contains(selector, capsule.RootDir+kpath) && c.Cert == nil {
|
|
return Response{STATUS_TRANSIENT_CERTIFICATE_REQUESTED, "Please provide a client certificate", ""}
|
|
}
|
|
}
|
|
for _, tpath := range capsule.AccessControl.Trusted {
|
|
match := strings.Contains(selector, capsule.RootDir+tpath)
|
|
if match && c.Cert == nil {
|
|
return Response{STATUS_AUTHORISED_CERTIFICATE_REQUIRED, "Please provide a client certificate", ""}
|
|
} else if match && validCert(capsule, c.Cert) != 0 {
|
|
return Response{validCert(capsule, c.Cert), "Error", ""}
|
|
}
|
|
}
|
|
|
|
fi, err := os.Stat(selector)
|
|
switch {
|
|
case err != nil:
|
|
// File doesn't exist.
|
|
return Response{STATUS_NOT_FOUND, "Couldn't find file", ""}
|
|
case os.IsNotExist(err) || os.IsPermission(err):
|
|
return Response{STATUS_NOT_FOUND, "File does not exist", ""}
|
|
case isNotWorldReadable(fi):
|
|
return Response{STATUS_TEMPORARY_FAILURE, "Unable to access file", ""}
|
|
case fi.IsDir():
|
|
if strings.HasSuffix(u.Path, "/") {
|
|
return generateDirectory(selector)
|
|
} else {
|
|
return Response{STATUS_REDIRECT_PERMANENT, "gemini://" + u.Hostname() + u.Path + "/", ""}
|
|
}
|
|
default:
|
|
// it's a file
|
|
match := strings.Contains(selector, capsule.RootDir+capsule.CGIDir)
|
|
if match && fi.Mode().Perm()&0111 == 0111 {
|
|
//CGI file found
|
|
return generateCGI(u, c)
|
|
} else {
|
|
//Normal file found
|
|
return generateFile(selector)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *conn) sendResponse(r Response) error {
|
|
c.C.Write([]byte(fmt.Sprintf("%v %v\r\n", r.Status, r.Meta)))
|
|
if r.Body != "" {
|
|
c.C.Write([]byte(r.Body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isNotWorldReadable(file os.FileInfo) bool {
|
|
return uint64(file.Mode().Perm())&0444 != 0444
|
|
}
|