Serve Gemini content via HTTPS

This commit is contained in:
Trevor Slocum 2020-12-03 11:12:13 -08:00
parent 19b89bfd9e
commit 8d6cb6527e
8 changed files with 372 additions and 111 deletions

View file

@ -71,11 +71,19 @@ certbot certonly --config-dir /home/www/certs \
Provide the path to the certificate file at `certs/live/$DOMAIN/fullchain.pem` Provide the path to the certificate file at `certs/live/$DOMAIN/fullchain.pem`
and the private key file at `certs/live/$DOMAIN/privkey.pem` to twins. and the private key file at `certs/live/$DOMAIN/privkey.pem` to twins.
## DisableHTTPS
Pages are also available via HTTPS on the same port by default.
Set this option to `true` to disable this feature.
Pages are converted automatically by [gmitohtml](https://gitlab.com/tslocum/gmitohtml).
### DisableSize ### DisableSize
The size of the response body is included in the media type header by default. The size of the response body is included in the media type header by default.
Set this option to `true` to disable this feature. See [PROPOSALS.md](https://gitlab.com/tslocum/twins/blob/master/PROPOSALS.md) Set this option to `true` to disable this feature.
for more information.
See [PROPOSALS.md](https://gitlab.com/tslocum/twins/blob/master/PROPOSALS.md) for more information.
### Path ### Path
@ -115,42 +123,6 @@ 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) out-of-spec feature. See [PROPOSALS.md](https://gitlab.com/tslocum/twins/blob/master/PROPOSALS.md)
for more information. for more information.
##### Hidden
When enabled, hidden files and directories may be accessed. This attribute is
disabled by default.
##### Input
Request text input from user.
##### SensitiveInput
Request sensitive text input from the user. Text will not be shown as it is entered.
##### List
When enabled, directories without an index file will serve a list of their
contents. This attribute is disabled by default.
##### Lang
Specifies content language. This is sent to clients via the MIME type `lang` parameter.
##### Log
Path to log file. Requests are logged in [Apache format](https://httpd.apache.org/docs/2.2/logs.html#combined),
excluding IP address and query.
##### SymLinks
When enabled, symbolic links may be accessed. This attribute is disabled by default.
##### Type
Content type is normally detected automatically. This attribute forces a
specific content type for a path.
##### FastCGI ##### FastCGI
Forward requests to [FastCGI](https://en.wikipedia.org/wiki/FastCGI) server at Forward requests to [FastCGI](https://en.wikipedia.org/wiki/FastCGI) server at
@ -170,6 +142,42 @@ Connect via TCP:
tcp://127.0.0.1:9000 tcp://127.0.0.1:9000
``` ```
##### Hidden
When enabled, hidden files and directories may be accessed. This attribute is
disabled by default.
##### Input
Request text input from user.
##### Lang
Specifies content language. This is sent to clients via the MIME type `lang` parameter.
##### List
When enabled, directories without an index file will serve a list of their
contents. This attribute is disabled by default.
##### Log
Path to log file. Requests are logged in [Apache format](https://httpd.apache.org/docs/2.2/logs.html#combined),
excluding IP address and query.
##### SensitiveInput
Request sensitive text input from the user. Text will not be shown as it is entered.
##### SymLinks
When enabled, symbolic links may be accessed. This attribute is disabled by default.
##### Type
Content type is normally detected automatically. This attribute forces a
specific content type for a path.
## End-of-line indicator ## End-of-line indicator
The Gemini protocol requires `\r\n` (CRLF) as the end-of-line indicator. This The Gemini protocol requires `\r\n` (CRLF) as the end-of-line indicator. This

View file

@ -18,8 +18,8 @@ This page is also available at [gemini://twins.rocketnine.space](gemini://twins.
- Reverse proxy requests - Reverse proxy requests
- TCP - TCP
- [FastCGI](https://en.wikipedia.org/wiki/FastCGI) - [FastCGI](https://en.wikipedia.org/wiki/FastCGI)
- Serve system command output - Serve Gemini content via HTTPS
- Redirect to path or URL - Pages are converted automatically by [gmitohtml](https://gitlab.com/tslocum/gmitohtml)
- Reload configuration on `SIGHUP` - Reload configuration on `SIGHUP`
## Proposals ## Proposals

View file

@ -26,36 +26,36 @@ type pathConfig struct {
Proxy string Proxy string
Redirect string Redirect string
// Cache duration
Cache string
// FastCGI server address
FastCGI string
// Serve hidden files and directories
Hidden bool
// Request input // Request input
Input string Input string
// Language
Lang string
// List directory
List bool
// Log file
Log string
// Request sensitive input // Request sensitive input
SensitiveInput string SensitiveInput string
// Follow symbolic links // Follow symbolic links
SymLinks bool SymLinks bool
// Serve hidden files and directories
Hidden bool
// List directory
List bool
// Content type // Content type
Type string Type string
// Cache duration
Cache string
// FastCGI server address
FastCGI string
// Language
Lang string
// Log file
Log string
r *regexp.Regexp r *regexp.Regexp
cmd []string cmd []string
cache int64 cache int64
@ -70,15 +70,12 @@ type hostConfig struct {
} }
type serverConfig struct { type serverConfig struct {
Listen string Listen string
Types map[string]string
Types map[string]string Hosts map[string]*hostConfig
DisableHTTPS bool
Hosts map[string]*hostConfig DisableSize bool
SaneEOL bool
DisableSize bool
SaneEOL bool
hostname string hostname string
port int port int
@ -167,36 +164,36 @@ func readconfig(configPath string) error {
if len(defaultHost.Paths) == 1 { if len(defaultHost.Paths) == 1 {
defaultPath := defaultHost.Paths[0] defaultPath := defaultHost.Paths[0]
for _, serve := range host.Paths { for _, serve := range host.Paths {
// Resources
if defaultPath.Root != "" && serve.Root == "" { if defaultPath.Root != "" && serve.Root == "" {
serve.Root = defaultPath.Root serve.Root = defaultPath.Root
} } else if defaultPath.Command != "" && serve.Command == "" {
if defaultPath.Command != "" && serve.Command == "" {
serve.Command = defaultPath.Command serve.Command = defaultPath.Command
} } else if defaultPath.Proxy != "" && serve.Proxy == "" {
if defaultPath.Proxy != "" && serve.Proxy == "" {
serve.Proxy = defaultPath.Proxy serve.Proxy = defaultPath.Proxy
} }
if defaultPath.SymLinks { // Attributes
serve.SymLinks = defaultPath.SymLinks
}
if defaultPath.Hidden {
serve.Hidden = defaultPath.Hidden
}
if defaultPath.List {
serve.List = defaultPath.List
}
if defaultPath.Cache != "" && serve.Cache == "" { if defaultPath.Cache != "" && serve.Cache == "" {
serve.Cache = defaultPath.Cache serve.Cache = defaultPath.Cache
} }
if defaultPath.FastCGI != "" && serve.FastCGI == "" { if defaultPath.FastCGI != "" && serve.FastCGI == "" {
serve.FastCGI = defaultPath.FastCGI serve.FastCGI = defaultPath.FastCGI
} }
if defaultPath.Hidden {
serve.Hidden = defaultPath.Hidden
}
if defaultPath.Lang != "" && serve.Lang == "" { if defaultPath.Lang != "" && serve.Lang == "" {
serve.Lang = defaultPath.Lang serve.Lang = defaultPath.Lang
} }
if defaultPath.List {
serve.List = defaultPath.List
}
if defaultPath.Log != "" && serve.Log == "" { if defaultPath.Log != "" && serve.Log == "" {
serve.Log = defaultPath.Log serve.Log = defaultPath.Log
} }
if defaultPath.SymLinks {
serve.SymLinks = defaultPath.SymLinks
}
} }
} else if len(defaultHost.Paths) > 1 { } else if len(defaultHost.Paths) > 1 {
log.Fatal("only one path may be defined for the default host") log.Fatal("only one path may be defined for the default host")

3
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/h2non/filetype v1.1.0 github.com/h2non/filetype v1.1.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107 github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107
golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11 // indirect gitlab.com/tslocum/gmitohtml v1.0.3-0.20201203184239-2a1abe8efe7c
golang.org/x/tools v0.0.0-20201203170353-bdde1628ed1d // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
) )

6
go.sum
View file

@ -12,6 +12,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107 h1:wfqP/vw5tHVeFQJbnmyXSi7E2ZeshpKhR4kuUR5B7yQ= github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107 h1:wfqP/vw5tHVeFQJbnmyXSi7E2ZeshpKhR4kuUR5B7yQ=
github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ= github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
gitlab.com/tslocum/gmitohtml v1.0.3-0.20201203184239-2a1abe8efe7c h1:hew6E9kPxaR/fipooKTLB+ARkpmI9ShtzYZFdKG7jzQ=
gitlab.com/tslocum/gmitohtml v1.0.3-0.20201203184239-2a1abe8efe7c/go.mod h1:+LFeUUQ6kcjFUR2y3XVr/4i8qLmZOUnM/gwsb9leHMk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -34,8 +36,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-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-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-20200908211811-12e1bf57a112/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11 h1:gqcmLJzeDSNhSzkyhJ4kxP6CtTimi/5hWFDGp0lFd1w= golang.org/x/tools v0.0.0-20201203170353-bdde1628ed1d h1:OuIGT9zWmMvqaajHPt4H4W1omjiKwkGpBw5ttuErmnw=
golang.org/x/tools v0.0.0-20201117152513-9036a0f9af11/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201203170353-bdde1628ed1d/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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -11,23 +11,23 @@ import (
"github.com/yookoala/gofast" "github.com/yookoala/gofast"
) )
type responseWriter struct { type fakeResponseWriter struct {
io.WriteCloser io.WriteCloser
header http.Header header http.Header
} }
func newResponseWriter(out io.WriteCloser) *responseWriter { func newFakeResponseWriter(out io.WriteCloser) *fakeResponseWriter {
return &responseWriter{ return &fakeResponseWriter{
WriteCloser: out, WriteCloser: out,
header: make(http.Header), header: make(http.Header),
} }
} }
func (w *responseWriter) Header() http.Header { func (w *fakeResponseWriter) Header() http.Header {
return w.header return w.header
} }
func (w *responseWriter) WriteHeader(statusCode int) { func (w *fakeResponseWriter) WriteHeader(statusCode int) {
// Do nothing // Do nothing
} }
@ -53,5 +53,5 @@ func serveFastCGI(c net.Conn, connFactory gofast.ConnFactory, u *url.URL, filePa
gofast.NewFileEndpoint(filePath)(gofast.BasicSession), gofast.NewFileEndpoint(filePath)(gofast.BasicSession),
gofast.SimpleClientFactory(connFactory, 0), gofast.SimpleClientFactory(connFactory, 0),
). ).
ServeHTTP(newResponseWriter(c), r) ServeHTTP(newFakeResponseWriter(c), r)
} }

212
serve_https.go Normal file
View file

@ -0,0 +1,212 @@
package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strings"
"gitlab.com/tslocum/gmitohtml/pkg/gmitohtml"
)
var cssBytes = []byte(gmitohtml.StyleCSS)
func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
if r.URL.Path == "" {
// Redirect to /
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, ""
}
u.Path += "/"
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, ""
} else if r.URL.Path == "/assets/style.css" {
status := http.StatusOK
w.Header().Set("Content-Type", cssType)
w.WriteHeader(status)
w.Write(cssBytes)
return status, int64(len(cssBytes)), ""
}
pathBytes := []byte(r.URL.Path)
strippedPath := r.URL.Path
if strippedPath[0] == '/' {
strippedPath = strippedPath[1:]
}
if host, ok := config.Hosts[r.URL.Hostname()]; ok {
for _, serve := range host.Paths {
matchedRegexp := serve.r != nil && serve.r.Match(pathBytes)
matchedPrefix := serve.r == nil && strings.HasPrefix(r.URL.Path, serve.Path)
if !matchedRegexp && !matchedPrefix {
continue
}
requireInput := serve.Input != "" || serve.SensitiveInput != ""
if r.URL.RawQuery == "" && requireInput {
if serve.SensitiveInput != "" {
// TODO
}
status := http.StatusInternalServerError
http.Error(w, "Gemini to HTML conversion is not supported for this page", status)
return status, -1, serve.Log
}
if matchedRegexp || matchedPrefix {
if serve.Root == "" || serve.FastCGI != "" {
status := http.StatusInternalServerError
http.Error(w, "Gemini to HTML conversion is not supported for this page", status)
return status, -1, serve.Log
}
var filePath string
if serve.Root != "" {
root := serve.Root
if root[len(root)-1] != '/' {
root += "/"
}
requestSplit := strings.Split(r.URL.Path, "/")
if !serve.SymLinks {
for i := 1; i < len(requestSplit); i++ {
info, err := os.Lstat(path.Join(root, strings.Join(requestSplit[1:i+1], "/")))
if err != nil || info.Mode()&os.ModeSymlink == os.ModeSymlink {
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
}
}
filePath = path.Join(root, strings.Join(requestSplit[1:], "/"))
}
fi, err := os.Stat(filePath)
if err != nil {
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
mode := fi.Mode()
hasTrailingSlash := len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] == '/'
if mode.IsDir() {
if !hasTrailingSlash {
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, serve.Log
}
u.Path += "/"
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, serve.Log
}
_, err := os.Stat(path.Join(filePath, "index.gmi"))
if err != nil {
_, err := os.Stat(path.Join(filePath, "index.gemini"))
if err != nil {
if serve.List {
status := http.StatusInternalServerError
http.Error(w, "HTTPS dir lost not yet implemented", status)
return status, -1, serve.Log
}
http.NotFound(w, r)
return http.StatusNotFound, -1, serve.Log
}
filePath = path.Join(filePath, "index.gemini")
} else {
filePath = path.Join(filePath, "index.gmi")
}
} else if hasTrailingSlash && len(r.URL.Path) > 1 {
u, err := url.Parse(r.URL.String())
if err != nil {
status := http.StatusInternalServerError
http.Error(w, "Failed to parse URL", status)
return status, -1, serve.Log
}
u.Path = u.Path[:len(u.Path)-1]
status := http.StatusTemporaryRedirect
http.Redirect(w, r, u.String(), status)
return status, -1, serve.Log
}
data, err := ioutil.ReadFile(filePath)
if err != nil {
status := http.StatusInternalServerError
http.Error(w, err.Error(), status)
return status, -1, serve.Log
}
result := gmitohtml.Convert([]byte(data), r.URL.String())
status := http.StatusOK
w.Header().Set("Content-Type", htmlType)
w.WriteHeader(status)
w.Write(result)
return status, int64(len(result)), serve.Log
}
}
}
http.NotFound(w, r)
return http.StatusNotFound, -1, ""
}
type responseWriter struct {
statusCode int
header http.Header
conn *tls.Conn
wroteHeader bool
}
func newResponseWriter(conn *tls.Conn) *responseWriter {
return &responseWriter{
header: http.Header{},
conn: conn,
}
}
func (w *responseWriter) Header() http.Header {
return w.header
}
func (w *responseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.wroteHeader = true
w.WriteHeader(http.StatusOK)
}
return w.conn.Write(b)
}
func (w *responseWriter) WriteHeader(statusCode int) {
if w.wroteHeader {
return
}
w.statusCode = statusCode
statusText := http.StatusText(statusCode)
if statusText == "" {
statusText = "Unknown"
}
w.conn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", statusCode, statusText)))
w.header.Write(w.conn)
w.conn.Write([]byte("\r\n"))
}

View file

@ -6,8 +6,10 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io"
"log" "log"
"net" "net"
"net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
@ -27,6 +29,7 @@ const (
plainType = "text/plain; charset=utf-8" plainType = "text/plain; charset=utf-8"
geminiType = "text/gemini; charset=utf-8" geminiType = "text/gemini; charset=utf-8"
htmlType = "text/html; charset=utf-8" htmlType = "text/html; charset=utf-8"
cssType = "text/css; charset=utf-8"
logTimeFormat = "2006-01-02 15:04:05" logTimeFormat = "2006-01-02 15:04:05"
) )
@ -303,13 +306,14 @@ func handleConn(c *tls.Conn) {
var logPath string var logPath string
status := 0 status := 0
size := int64(-1) size := int64(-1)
protocol := "Gemini"
defer func() { defer func() {
if quiet && logPath == "" { if quiet && logPath == "" {
return return
} }
entry := logEntry(request, status, size, time.Since(t)) entry := logEntry(protocol, request, status, size, time.Since(t))
if !quiet { if !quiet {
log.Println(string(entry)) log.Println(string(entry))
@ -336,21 +340,33 @@ func handleConn(c *tls.Conn) {
f.Write([]byte("\n")) f.Write([]byte("\n"))
}() }()
defer c.Close()
c.SetReadDeadline(time.Now().Add(readTimeout)) c.SetReadDeadline(time.Now().Add(readTimeout))
defer c.Close()
var requestData string var dataBuf []byte
scanner := bufio.NewScanner(c) var buf = make([]byte, 1)
if !config.SaneEOL { var readCR bool
scanner.Split(scanCRLF) if config.SaneEOL {
readCR = true
} }
if scanner.Scan() { for {
requestData = scanner.Text() n, err := c.Read(buf)
} if err == io.EOF {
if err := scanner.Err(); err != nil { break
status = writeStatus(c, statusBadRequest) } else if err != nil || n != 1 {
return return
}
if buf[0] == '\r' {
readCR = true
continue
} else if readCR && buf[0] == '\n' {
break
}
dataBuf = append(dataBuf, buf[0])
} }
requestData := string(dataBuf)
state := c.ConnectionState() state := c.ConnectionState()
certs := state.PeerCertificates certs := state.PeerCertificates
@ -363,6 +379,36 @@ func handleConn(c *tls.Conn) {
clientCertKeys = append(clientCertKeys, pubKey) clientCertKeys = append(clientCertKeys, pubKey)
} }
if strings.HasPrefix(requestData, "GET ") {
w := newResponseWriter(c)
defer w.WriteHeader(http.StatusOK)
if config.DisableHTTPS {
status = statusProxyRequestRefused
http.Error(w, "Error: Proxy request refused", http.StatusBadRequest)
return
}
r, err := http.ReadRequest(bufio.NewReader(io.MultiReader(strings.NewReader(requestData+"\r\n"), c)))
if err != nil {
status = http.StatusInternalServerError
http.Error(w, err.Error(), status)
return
}
if r.Proto == "" {
protocol = "HTTP"
} else {
protocol = r.Proto
}
r.URL.Scheme = "https"
r.URL.Host = strings.ToLower(r.Host)
request = r.URL
status, size, logPath = serveHTTPS(w, r)
return
}
if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) { if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) {
status = writeStatus(c, statusBadRequest) status = writeStatus(c, statusBadRequest)
return return
@ -374,6 +420,7 @@ func handleConn(c *tls.Conn) {
status = writeStatus(c, statusBadRequest) status = writeStatus(c, statusBadRequest)
return return
} }
request.Host = strings.ToLower(request.Host)
requestHostname := request.Hostname() requestHostname := request.Hostname()
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') { if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
@ -389,22 +436,16 @@ func handleConn(c *tls.Conn) {
} }
} }
if request.Scheme == "" { validScheme := request.Scheme == "gemini" || (!config.DisableHTTPS && request.Scheme == "https")
request.Scheme = "gemini" if !validScheme || (requestPort > 0 && requestPort != config.port) {
}
if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) {
status = writeStatus(c, statusProxyRequestRefused) status = writeStatus(c, statusProxyRequestRefused)
return return
} }
if request.Scheme == "gemini" {
request.Host = strings.ToLower(request.Host)
}
status, size, logPath = handleRequest(c, request, requestData) status, size, logPath = handleRequest(c, request, requestData)
} }
func logEntry(request *url.URL, status int, size int64, elapsed time.Duration) []byte { func logEntry(protocol string, request *url.URL, status int, size int64, elapsed time.Duration) []byte {
hostFormatted := "-" hostFormatted := "-"
pathFormatted := "-" pathFormatted := "-"
sizeFormatted := "-" sizeFormatted := "-"
@ -424,7 +465,7 @@ func logEntry(request *url.URL, status int, size int64, elapsed time.Duration) [
if size >= 0 { if size >= 0 {
sizeFormatted = strconv.FormatInt(size, 10) sizeFormatted = strconv.FormatInt(size, 10)
} }
return []byte(fmt.Sprintf(`%s - - - [%s] "GET %s Gemini" %d %s %.4f`, hostFormatted, time.Now().Format(logTimeFormat), pathFormatted, status, sizeFormatted, elapsed.Seconds())) return []byte(fmt.Sprintf(`%s - - - [%s] "GET %s %s" %d %s %.4f`, hostFormatted, time.Now().Format(logTimeFormat), pathFormatted, protocol, status, sizeFormatted, elapsed.Seconds()))
} }
func handleListener(l net.Listener) { func handleListener(l net.Listener) {