diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 262f73b..5369451 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -65,7 +65,8 @@ and the private key file at `certs/live/$DOMAIN/privkey.pem` to twins. ### DisableSize The size of the response body is included in the media type header by default. -Set this option to `true` to disable this feature. +Set this option to `true` to disable this feature. See [PROPOSALS.md](https://gitlab.com/tslocum/twins/blob/master/PROPOSALS.md) +for more information. ### Path @@ -95,6 +96,12 @@ argument to the command, otherwise user input is passed via standard input. Any number of attributes may be defined for a path. +##### Cache + +Cache duration (in seconds). Set to `0` to disable caching entirely. This is an +out-of-spec feature. See [PROPOSALS.md](https://gitlab.com/tslocum/twins/blob/master/PROPOSALS.md) +for more information. + ##### ListDirectory Directory listing may be enabled by adding `listdirectory: true`. @@ -124,9 +131,8 @@ A `Root` attribute must also be specified to use `FastCGI`. The Gemini protocol requires `\r\n` (CRLF) as the end-of-line indicator. This convention is carried over from protocol specifications **first written in the 1970s**. This requirement is antithetic to the spirit of Gemini (to improve -upon the Finger and Gopher protocols) because it unnecessarily tacks on ancient -baggage. This baggage has caused (and continues to cause) increased complexity in -client and server implementations, which naturally gives rise to more bugs. +upon the Finger and Gopher protocols), increasing the complexity of client and +server implementations unnecessarily. In anticipation of an improvement to the Gemini specification, administrators may configure twins to send standard `\n` (LF) line endings by setting @@ -157,6 +163,7 @@ hosts: - path: /sites root: /home/geminirocks/data + cache: 604800 # Cache for 1 week listdirectory: true - path: ^/(help|info)$ @@ -167,6 +174,7 @@ hosts: - path: ^/cmd-example$ command: uname -a + cache: 0 # Do not cache - path: / root: /home/geminirocks/data/home diff --git a/config.go b/config.go index ca68fc1..12af4c5 100644 --- a/config.go +++ b/config.go @@ -37,6 +37,9 @@ type pathConfig struct { // Content type Type string + // Cache duration + Cache int64 `default:"-1965"` + // FastCGI server address FastCGI string diff --git a/go.mod b/go.mod index d9b483d..5757b69 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,6 @@ require ( github.com/h2non/filetype v1.1.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107 - golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b // indirect + golang.org/x/tools v0.0.0-20201110030525-169ad6d6ecb2 // indirect gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 ) diff --git a/go.sum b/go.sum index 97afc8f..b5146bc 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200908211811-12e1bf57a112/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b h1:ILx+nYAUTq4JtXxrXKQ82j7nruEwlXRfUn+kxtsnElg= -golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110030525-169ad6d6ecb2 h1:5GmCe1Mc5HsGGl6E0kOVQRzVp+AgZf4Ffw4DadiVpd4= +golang.org/x/tools v0.0.0-20201110030525-169ad6d6ecb2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/serve_command.go b/serve_command.go index c7b1b29..8cd85d0 100644 --- a/serve_command.go +++ b/serve_command.go @@ -9,7 +9,7 @@ import ( "strings" ) -func serveCommand(c net.Conn, request *url.URL, command []string) { +func serveCommand(c net.Conn, serve *pathConfig, request *url.URL, command []string) { var args []string if len(command) > 0 { args = command[1:] @@ -34,7 +34,7 @@ func serveCommand(c net.Conn, request *url.URL, command []string) { return } - writeHeader(c, statusSuccess, "text/gemini; charset=utf-8") + writeSuccess(c, serve, geminiType, int64(buf.Len())) c.Write(buf.Bytes()) if verbose { diff --git a/serve_file.go b/serve_file.go index 5fefd85..85a7942 100644 --- a/serve_file.go +++ b/serve_file.go @@ -13,7 +13,7 @@ import ( "github.com/h2non/filetype" ) -func serveDirList(c net.Conn, request *url.URL, dirPath string) { +func serveDirList(c net.Conn, serve *pathConfig, request *url.URL, dirPath string) { var ( files []os.FileInfo numDirs int @@ -50,7 +50,7 @@ func serveDirList(c net.Conn, request *url.URL, dirPath string) { return i < j }) - writeHeader(c, statusSuccess, "text/gemini; charset=utf-8") + writeSuccess(c, serve, geminiType, -1) fmt.Fprintf(c, "# %s%s", request.Path, newLine) if numDirs == 1 { @@ -93,7 +93,7 @@ func serveDirList(c net.Conn, request *url.URL, dirPath string) { } } -func serveFile(c net.Conn, filePath string) { +func serveFile(c net.Conn, serve *pathConfig, filePath string) { // Open file file, _ := os.Open(filePath) defer file.Close() @@ -103,32 +103,26 @@ func serveFile(c net.Conn, filePath string) { n, _ := file.Read(buf) // Write response header + var contentType string + if strings.HasSuffix(filePath, ".html") && strings.HasSuffix(filePath, ".htm") { + contentType = "text/html; charset=utf-8" + } else if strings.HasSuffix(filePath, ".txt") && strings.HasSuffix(filePath, ".text") { + contentType = "text/plain; charset=utf-8" + } else if !strings.HasSuffix(filePath, ".gmi") && !strings.HasSuffix(filePath, ".gemini") { + kind, _ := filetype.Match(buf[:n]) + if kind != filetype.Unknown { + contentType = kind.MIME.Value + } + } + if contentType == "" { + contentType = geminiType + } size := int64(-1) info, err := file.Stat() if err == nil { size = info.Size() } - 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" - } - var meta string - if !config.DisableSize && size >= 0 { - meta = fmt.Sprintf("%s; size=%d", mimeType, size) - } else { - meta = mimeType - } - writeHeader(c, statusSuccess, meta) + writeSuccess(c, serve, contentType, size) // Write body c.Write(buf[:n]) diff --git a/server.go b/server.go index 58fe699..e132be6 100644 --- a/server.go +++ b/server.go @@ -22,6 +22,8 @@ const ( readTimeout = 30 * time.Second urlMaxLength = 1024 + + geminiType = "text/gemini; charset=utf-8" ) const ( @@ -74,6 +76,23 @@ func writeStatus(c net.Conn, code int) { writeHeader(c, code, meta) } +func writeSuccess(c net.Conn, serve *pathConfig, contentType string, size int64) { + meta := contentType + if serve.Type != "" { + meta = serve.Type + } + + if !config.DisableSize && size >= 0 { + meta += fmt.Sprintf("; size=%d", size) + } + + if serve.Cache != -1965 { + meta += fmt.Sprintf("; cache=%d", serve.Cache) + } + + writeHeader(c, statusSuccess, meta) +} + func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { return 0, nil, nil @@ -138,11 +157,11 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) { return } - contentType := "text/gemini; charset=utf-8" + contentType := geminiType if serve.Type != "" { contentType = serve.Type } - writeHeader(c, statusSuccess, contentType) + writeSuccess(c, serve, contentType, -1) serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath) return @@ -151,11 +170,11 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) { if requireInput { newCommand := replaceWithUserInput(serve.cmd, request) if newCommand != nil { - serveCommand(c, request, newCommand) + serveCommand(c, serve, request, newCommand) return } } - serveCommand(c, request, serve.cmd) + serveCommand(c, serve, request, serve.cmd) return } @@ -183,7 +202,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) { _, err := os.Stat(path.Join(filePath, "index.gemini")) if err != nil { if serve.ListDirectory { - serveDirList(c, request, filePath) + serveDirList(c, serve, request, filePath) return } writeStatus(c, statusNotFound) @@ -199,7 +218,7 @@ func servePath(c *tls.Conn, request *url.URL, serve *pathConfig) { return } - serveFile(c, filePath) + serveFile(c, serve, filePath) } func serveConn(c *tls.Conn) {