Specify path using fixed string or regular expression

This commit is contained in:
Trevor Slocum 2020-10-29 14:58:12 -07:00
parent ba5b3dc5f0
commit d66e5d6384
4 changed files with 207 additions and 185 deletions

View File

@ -1,3 +1,7 @@
Paths may be defined as fixed strings or regular expressions (starting with `^`).
Fixed string paths will match with and without a trailing slash.
# config.yaml
```yaml
@ -8,12 +12,12 @@ key: /home/twins/data/keyfile.key
# Paths to serve
serve:
-
dir: /sites
path: /sites
root: /home/twins/data/sites
-
regexp: ^/(help|info)$
path: ^/(help|info)$
root: /home/twins/data/help
-
dir: /
path: /
root: /home/twins/data/home
```

View File

@ -10,17 +10,17 @@ import (
)
type serveConfig struct {
Dir string
Regexp string
Root string
Path string
Root string
r *regexp.Regexp
}
type serverConfig struct {
Cert string
Key string
Serve []*serveConfig
Cert string
Key string
Address string
Serve []*serveConfig
}
var config = &serverConfig{}
@ -45,11 +45,14 @@ func readconfig(configPath string) error {
}
for _, serve := range config.Serve {
if serve.Dir != "" && serve.Dir[len(serve.Dir)-1] == '/' {
serve.Dir = serve.Dir[:len(serve.Dir)-1]
if serve.Path == "" {
continue
}
if serve.Regexp != "" {
serve.r = regexp.MustCompile(serve.Regexp)
if serve.Path[0] == '^' {
serve.r = regexp.MustCompile(serve.Path)
} else if serve.Path[len(serve.Path)-1] == '/' {
serve.Path = serve.Path[:len(serve.Path)-1]
}
}

176
main.go
View File

@ -1,165 +1,14 @@
package main
import (
"bufio"
"bytes"
"crypto/tls"
"flag"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"path"
"strings"
"github.com/h2non/filetype"
"github.com/makeworld-the-better-one/go-gemini"
)
func writeHeader(c net.Conn, code int, meta string) {
fmt.Fprintf(c, "%d %s\r\n", code, meta)
}
func respond(c net.Conn, code int) {
var meta string
switch code {
case gemini.StatusTemporaryFailure:
meta = "Temporary failure"
case gemini.StatusBadRequest:
meta = "Bad request"
case gemini.StatusNotFound:
meta = "Not found"
}
writeHeader(c, code, meta)
}
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\r'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
func handleConn(c net.Conn) {
defer c.Close()
var requestData string
scanner := bufio.NewScanner(c)
scanner.Split(scanCRLF)
if scanner.Scan() {
requestData = scanner.Text()
}
if err := scanner.Err(); err != nil {
respond(c, gemini.StatusBadRequest)
return
}
request, err := url.Parse(requestData)
if err != nil || request.Scheme != "gemini" || request.Host == "" {
respond(c, gemini.StatusBadRequest)
return
}
if request.Path == "" {
request.Path = "/"
}
pathBytes := []byte(request.Path)
strippedPath := request.Path
if strippedPath[0] == '/' {
strippedPath = strippedPath[1:]
}
for _, serve := range config.Serve {
var realPath string
if serve.Dir != "" && strings.HasPrefix(request.Path, serve.Dir) {
realPath = path.Join(serve.Root, request.Path[len(serve.Dir):])
} else if serve.r != nil && serve.r.Match(pathBytes) {
realPath = path.Join(serve.Root, strippedPath)
} else {
continue
}
fi, err := os.Stat(realPath)
if os.IsNotExist(err) {
respond(c, gemini.StatusNotFound)
return
} else if err != nil {
respond(c, gemini.StatusTemporaryFailure)
return
}
if mode := fi.Mode(); mode.IsDir() {
_, err := os.Stat(path.Join(realPath, "index.gemini"))
if err == nil {
realPath = path.Join(realPath, "index.gemini")
} else {
realPath = path.Join(realPath, "index.gmi")
}
}
fi, err = os.Stat(realPath)
if os.IsNotExist(err) {
respond(c, gemini.StatusNotFound)
return
} else if err != nil {
respond(c, gemini.StatusTemporaryFailure)
return
}
file, _ := os.Open(realPath)
defer file.Close()
buf := make([]byte, 261)
n, _ := file.Read(buf)
mimeType := "text/gemini; charset=utf-8"
if !strings.HasSuffix(realPath, ".gmi") && !strings.HasSuffix(realPath, ".gemini") {
if strings.HasSuffix(realPath, ".html") && strings.HasSuffix(realPath, ".htm") {
mimeType = "text/html; charset=utf-8"
} else if strings.HasSuffix(realPath, ".txt") && strings.HasSuffix(realPath, ".text") {
mimeType = "text/plain; charset=utf-8"
} else {
kind, _ := filetype.Match(buf[:n])
if kind != filetype.Unknown {
mimeType = kind.MIME.Value
}
}
}
writeHeader(c, gemini.StatusSuccess, mimeType)
c.Write(buf[:n])
io.Copy(c, file)
return
}
respond(c, gemini.StatusNotFound)
}
func handleListener(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
func main() {
configFile := flag.String("config", "", "path to configuration file")
certFile := flag.String("cert", "", "path to certificate file")
keyFile := flag.String("key", "", "path to private key file")
flag.Parse()
if *configFile == "" {
@ -170,34 +19,17 @@ func main() {
}
err := readconfig(*configFile)
if err != nil && *certFile == "" {
if err != nil {
log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring twins", *configFile, err)
}
if *certFile != "" {
config.Cert = *certFile
}
if *keyFile != "" {
config.Key = *keyFile
if config.Address == "" {
log.Fatal("listen address must be specified")
}
if config.Cert == "" || config.Key == "" {
log.Fatal("certificate file and private key must be specified (gemini requires TLS for all connections)")
}
cert, err := tls.LoadX509KeyPair(config.Cert, config.Key)
if err != nil {
log.Fatalf("failed to load certificate: %s", err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener, err := tls.Listen("tcp", "localhost:8888", tlsConfig)
if err != nil {
log.Fatalf("failed to listen on %s: %s", "localhost:8888", err)
}
handleListener(listener)
listen(config.Address, config.Cert, config.Key)
}

183
server.go Normal file
View File

@ -0,0 +1,183 @@
package main
import (
"bufio"
"bytes"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"path"
"strings"
"github.com/h2non/filetype"
"github.com/makeworld-the-better-one/go-gemini"
)
func writeHeader(c net.Conn, code int, meta string) {
fmt.Fprintf(c, "%d %s\r\n", code, meta)
}
func writeStatus(c net.Conn, code int) {
var meta string
switch code {
case gemini.StatusTemporaryFailure:
meta = "Temporary failure"
case gemini.StatusBadRequest:
meta = "Bad request"
case gemini.StatusNotFound:
meta = "Not found"
}
writeHeader(c, code, meta)
}
func serveFile(c net.Conn, filePath string) {
fi, err := os.Stat(filePath)
if os.IsNotExist(err) {
writeStatus(c, gemini.StatusNotFound)
return
} else if err != nil {
writeStatus(c, gemini.StatusTemporaryFailure)
return
}
if mode := fi.Mode(); mode.IsDir() {
_, err := os.Stat(path.Join(filePath, "index.gemini"))
if err == nil {
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
}
fi, err = os.Stat(filePath)
if os.IsNotExist(err) {
writeStatus(c, gemini.StatusNotFound)
return
} else if err != nil {
writeStatus(c, gemini.StatusTemporaryFailure)
return
}
// Open file
file, _ := os.Open(filePath)
defer file.Close()
// Read file header
buf := make([]byte, 261)
n, _ := file.Read(buf)
// Write header
var mimeType string
if strings.HasSuffix(filePath, ".html") && strings.HasSuffix(filePath, ".htm") {
mimeType = "text/html; charset=utf-8"
} else if strings.HasSuffix(filePath, ".txt") && strings.HasSuffix(filePath, ".text") {
mimeType = "text/plain; charset=utf-8"
} else if !strings.HasSuffix(filePath, ".gmi") && !strings.HasSuffix(filePath, ".gemini") {
kind, _ := filetype.Match(buf[:n])
if kind != filetype.Unknown {
mimeType = kind.MIME.Value
}
}
if mimeType == "" {
mimeType = "text/gemini; charset=utf-8"
}
writeHeader(c, gemini.StatusSuccess, mimeType)
// Write body
c.Write(buf[:n])
io.Copy(c, file)
}
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\r'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0:i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
func handleConn(c net.Conn) {
defer c.Close()
var requestData string
scanner := bufio.NewScanner(c)
scanner.Split(scanCRLF)
if scanner.Scan() {
requestData = scanner.Text()
}
if err := scanner.Err(); err != nil {
writeStatus(c, gemini.StatusBadRequest)
return
}
request, err := url.Parse(requestData)
if err != nil || request.Scheme != "gemini" || request.Host == "" {
writeStatus(c, gemini.StatusBadRequest)
return
}
if request.Path == "" {
request.Path = "/"
}
pathBytes := []byte(request.Path)
strippedPath := request.Path
if strippedPath[0] == '/' {
strippedPath = strippedPath[1:]
}
var realPath string
for _, serve := range config.Serve {
if serve.r != nil && serve.r.Match(pathBytes) {
realPath = path.Join(serve.Root, strippedPath)
} else if serve.r == nil && strings.HasPrefix(request.Path, serve.Path) {
realPath = path.Join(serve.Root, request.Path[len(serve.Path):])
} else {
continue
}
serveFile(c, realPath)
return
}
writeStatus(c, gemini.StatusNotFound)
}
func handleListener(l net.Listener) {
for {
conn, err := l.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
func listen(address, certFile, keyFile string) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalf("failed to load certificate: %s", err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener, err := tls.Listen("tcp", address, tlsConfig)
if err != nil {
log.Fatalf("failed to listen on %s: %s", address, err)
}
handleListener(listener)
}