mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 15:28:13 +01:00
Specify path using fixed string or regular expression
This commit is contained in:
parent
ba5b3dc5f0
commit
d66e5d6384
4 changed files with 207 additions and 185 deletions
|
@ -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
|
# config.yaml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -8,12 +12,12 @@ key: /home/twins/data/keyfile.key
|
||||||
# Paths to serve
|
# Paths to serve
|
||||||
serve:
|
serve:
|
||||||
-
|
-
|
||||||
dir: /sites
|
path: /sites
|
||||||
root: /home/twins/data/sites
|
root: /home/twins/data/sites
|
||||||
-
|
-
|
||||||
regexp: ^/(help|info)$
|
path: ^/(help|info)$
|
||||||
root: /home/twins/data/help
|
root: /home/twins/data/help
|
||||||
-
|
-
|
||||||
dir: /
|
path: /
|
||||||
root: /home/twins/data/home
|
root: /home/twins/data/home
|
||||||
```
|
```
|
15
config.go
15
config.go
|
@ -10,8 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type serveConfig struct {
|
type serveConfig struct {
|
||||||
Dir string
|
Path string
|
||||||
Regexp string
|
|
||||||
Root string
|
Root string
|
||||||
|
|
||||||
r *regexp.Regexp
|
r *regexp.Regexp
|
||||||
|
@ -20,6 +19,7 @@ type serveConfig struct {
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
Cert string
|
Cert string
|
||||||
Key string
|
Key string
|
||||||
|
Address string
|
||||||
Serve []*serveConfig
|
Serve []*serveConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,11 +45,14 @@ func readconfig(configPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, serve := range config.Serve {
|
for _, serve := range config.Serve {
|
||||||
if serve.Dir != "" && serve.Dir[len(serve.Dir)-1] == '/' {
|
if serve.Path == "" {
|
||||||
serve.Dir = serve.Dir[:len(serve.Dir)-1]
|
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
176
main.go
|
@ -1,165 +1,14 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"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() {
|
func main() {
|
||||||
configFile := flag.String("config", "", "path to configuration file")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
if *configFile == "" {
|
if *configFile == "" {
|
||||||
|
@ -170,34 +19,17 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := readconfig(*configFile)
|
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)
|
log.Fatalf("failed to read configuration file at %s: %v\nSee CONFIGURATION.md for information on configuring twins", *configFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *certFile != "" {
|
if config.Address == "" {
|
||||||
config.Cert = *certFile
|
log.Fatal("listen address must be specified")
|
||||||
}
|
|
||||||
if *keyFile != "" {
|
|
||||||
config.Key = *keyFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Cert == "" || config.Key == "" {
|
if config.Cert == "" || config.Key == "" {
|
||||||
log.Fatal("certificate file and private key must be specified (gemini requires TLS for all connections)")
|
log.Fatal("certificate file and private key must be specified (gemini requires TLS for all connections)")
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(config.Cert, config.Key)
|
listen(config.Address, 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)
|
|
||||||
}
|
}
|
||||||
|
|
183
server.go
Normal file
183
server.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue