secretshop/server.go

233 lines
6.1 KiB
Go

package main
import (
"bufio"
"context"
"crypto/rand"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"mime"
"net"
"net/url"
"os"
"path/filepath"
"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
// Gopher 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
Hostname string // FQDN Hostname to reach this server on
ServerRoot string //Root folder for gemini files
}
type conn struct {
server *Server
C net.Conn
tlsState *tls.ConnectionState
}
func (s *Server) newConn(rwc net.Conn) *conn {
c := &conn{
server: s,
C: rwc,
tlsState: nil,
}
return c
}
func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
addr := s.Addr
mime.AddExtensionType(".gmi", "text/gemini")
mime.AddExtensionType(".gemini", "text/gemini")
if addr == "" {
addr = ":1965"
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalf("server: loadkeys: %s", err)
}
config := tls.Config{Certificates: []tls.Certificate{cert}}
config.Rand = rand.Reader
ln, err := tls.Listen("tcp", addr, &config)
if err != nil {
log.Fatalf("server: listen: %s", err)
}
return s.Serve(ln)
}
func ListenAndServeTLS(cp GeminiConfig) error {
server := &Server{Addr: ":" + cp.Port, ServerRoot: cp.RootDir, Hostname: cp.Hostname}
return server.ListenAndServeTLS(cp.CertFile, cp.KeyFile)
}
func (s *Server) Serve(l net.Listener) 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
}
c := s.newConn(rw)
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
//req, overflow, err := req_buf.ReadLine()
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", err.Error())
c.C.Close()
return
}
data[count] = b
count = count + 1
}
var res Response
var req string
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.sendResponse(res)
log.Printf("%v: %v requested %v; responded with %v %v", c.server.Hostname, c.C.RemoteAddr(), req, res.Status, res.Meta)
c.C.Close()
}
func (s *Server) ParseRequest(req string) 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 u.Hostname() != s.Hostname {
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.", ""}
}
selector := s.ServerRoot + u.Path
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 uint64(fi.Mode().Perm())&0444 != 0444:
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://" + s.Hostname + u.Path + "/", ""}
}
default:
// it's a file
return generateFile(selector)
}
}
func generateFile(selector string) Response {
meta := mime.TypeByExtension(filepath.Ext(selector))
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 listing string
files, err := ioutil.ReadDir(path)
if err != nil {
log.Println(err)
return Response{STATUS_TEMPORARY_FAILURE, "Unable to show directory listing", ""}
}
// Unashamedly based off solderpunks directory generation code
// https://tildegit.org/solderpunk/molly-brown/src/branch/master/handler.go
listing = "# Directory listing\r\n"
for _, file := range files {
// Skip dotfiles
if strings.HasPrefix(file.Name(), ".") {
continue
}
// Only list world readable files
if uint64(file.Mode().Perm())&0444 != 0444 {
continue
}
if file.Name() == "index.gmi" || file.Name() == "index.gemini" {
//Found an index file, return that instead
return generateFile(path + file.Name())
} else {
listing += fmt.Sprintf("=> %s %s\r\n", file.Name(), file.Name())
}
}
return Response{STATUS_SUCCESS, "text/gemini", listing}
}
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
}