Compare commits

...

15 Commits

Author SHA1 Message Date
Trevor Slocum c026f5c7b1 Clarify IPv4 and IPv6 configuration 2023-05-06 10:49:39 -07:00
Ivan Vilata-i-Balaguer 668e099ffa Add quotes for IPv6 address configuration example
Since leaving the quotes out in the config file causes the confusing "did not
find expected key" error, so do not assume that the user is familiar enough
with YAML syntax as to identify the issue.
2023-05-04 09:23:31 +02:00
Ivan Vilata-i-Balaguer 066fe276eb Fix listening on literal IPv6 address
Those contain colons, so splitting by colons would break the address in the
middle.  Instead, use a regular expression to split host and port at the last
colon, if followed by port digits and the end of the string.

Also, document the syntax for listening on literal IPv4 and IPv6 addresses.
2023-05-03 14:22:19 +02:00
Trevor Slocum 24f3196a61 Add configuration option ShowImages
When enabled, clients accessing gemini pages via HTTPS will see links to
images as inline images.

Resolves #13.
2021-07-22 20:02:42 -07:00
Trevor Slocum 0c4c7e8ecb Fix sending custom content types to HTTP clients
Resolves #14.
2021-07-22 19:49:24 -07:00
Trevor Slocum 12aa024671 Downgrade dependency yaml to v2
v3 has not been released yet.
2021-07-22 13:04:42 -07:00
tslocum 30d4097086 Merge pull request 'Update gmitohtml to 1.0.5 to support images' (#12) from f/twins:update-dependencies into master
Reviewed-on: https://code.rocketnine.space/tslocum/twins/pulls/12
2021-07-22 12:56:13 -07:00
Aaron Fischer 5632fd1ba4 Update gmitohtml to 1.0.5 to support images 2021-07-22 21:38:16 +02:00
Trevor Slocum 3500533ecf Update StyleSheet implementation 2021-07-10 09:27:52 -07:00
tslocum 9cd235c469 Merge pull request 'Add support for custom CSS files' (#11) from f/twins:custom-css into master
Reviewed-on: https://code.rocketnine.space/tslocum/twins/pulls/11
2021-07-09 19:37:56 -07:00
f 2252fc45dd Merge branch 'master' into custom-css 2021-07-09 13:21:40 -07:00
Aaron Fischer daf4b2b1a2 gofmt 2021-07-09 22:19:46 +02:00
Aaron Fischer 8654f0ca1b Rename to StyleSheet and precache CSS bytes
If there is a custom CSS file specified and usable (it exists and is
readable), we precache it on server startup (for every host). This
increase the request performance and reduce IO-load slightly.
2021-07-09 22:15:35 +02:00
Aaron Fischer 83453281bc Add support for custom CSS files
Specify the ```styles``` config option for a host and point to a
valid CSS file to use this instead of the default one. This fixes #9.
2021-06-08 22:10:10 +02:00
Trevor Slocum f8a14016f0 Update certificate generation command 2021-06-03 22:39:56 -07:00
8 changed files with 118 additions and 62 deletions

View File

@ -1,21 +0,0 @@
image: golang:latest
stages:
- validate
- build
fmt:
stage: validate
script:
- gofmt -l -s -e .
- exit $(gofmt -l -s -e . | wc -l)
vet:
stage: validate
script:
- go vet -composites=false ./...
test:
stage: validate
script:
- go test -race -v ./...

View File

@ -10,19 +10,29 @@ via the `--config` argument.
Address to listen for connections on in the format of `interface:port`.
### Listen on localhost
`localhost:1965`
### Listen on all interfaces
### Listen on all addresses (IPv4 and IPv6)
`:1965`
### Listen on a specific address (IPv4 or IPv6)
`192.0.2.1:1965` or `"[2001:db8::1]:1965"` (please note the quotes and brackets
for IPv6 addresses).
### Listen on localhost (IPv4 only)
`localhost:1965`
## Types
Content types may be defined by file extension. When a type is not defined for
the requested file extension, content type is detected automatically.
## ShowImages
When enabled, clients accessing gemini pages via HTTPS will see links to images
as inline images.
## Hosts
Hosts are defined by their hostname followed by one or more paths to serve.
@ -46,18 +56,35 @@ enable an attribute by default and then disable it for individual paths.
A certificate and private key must be specified.
#### localhost certificate
#### Self-signed domain certificate (recommended)
Use `openssl` generate a certificate for localhost.
Use `openssl` generate a certificate for a domain.
Replace `rocketnine.space` and `twins.rocketnine.space` with your domain and subdomain.
```bash
openssl req -x509 -out localhost.crt -keyout localhost.key \
-newkey rsa:2048 -nodes -sha256 \
-days 36500 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=rocketnine.space\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:rocketnine.space,DNS:twins.rocketnine.space\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
```
#### Self-signed localhost certificate
Use `openssl` generate a certificate for localhost.
Some Gemini clients will not accept such a certificate when accessing a server via domain or subdomain.
```bash
openssl req -x509 -out localhost.crt -keyout localhost.key \
-newkey rsa:2048 -nodes -sha256 \
-days 36500 \
-subj '/CN=localhost' -extensions EXT -config <( \
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
```
#### Domain certificate
#### Signed certificate from Let's Encrypt
Use [certbot](https://certbot.eff.org) to get a certificate from [Let's Encrypt](https://letsencrypt.org) for a domain.
@ -74,7 +101,13 @@ certbot certonly --config-dir /home/www/certs \
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.
## DisableHTTPS
### StyleSheet
Provide the path to a style sheet to serve instead of the default style sheet.
This option only applies when serving HTTPS connections.
### DisableHTTPS
Pages are also available via HTTPS on the same port by default.
Set this option to `true` to disable this feature.
@ -211,6 +244,7 @@ listen: :1965
# Custom content types
types:
.json: application/json; charset=UTF-8
.js: application/javascript; charset=UTF-8
# Hosts and paths to serve
hosts:
@ -223,6 +257,7 @@ hosts:
gemini.rocks:
cert: /srv/gemini.rocks/data/cert.crt
key: /srv/gemini.rocks/data/cert.key
stylesheet: /srv/gemini.rocks/style.css
paths:
-
path: ^/.*\.php$

View File

@ -11,9 +11,10 @@ import (
"strconv"
"strings"
"code.rocketnine.space/tslocum/gmitohtml/pkg/gmitohtml"
"github.com/kballard/go-shellquote"
"github.com/yookoala/gofast"
"gopkg.in/yaml.v3"
"gopkg.in/yaml.v2"
)
type pathConfig struct {
@ -66,12 +67,17 @@ type hostConfig struct {
Key string
Paths []*pathConfig
// Custom CSS styles. If specified, it will be used for all paths in that host/domain.
StyleSheet string
cert *tls.Certificate
css []byte
}
type serverConfig struct {
Listen string
Types map[string]string
ShowImages bool
Hosts map[string]*hostConfig
DisableHTTPS bool
DisableSize bool
@ -121,13 +127,13 @@ func readconfig(configPath string) error {
newLine = "\r\n"
}
split := strings.Split(config.Listen, ":")
if len(split) != 2 {
listenRe := regexp.MustCompile("(.*):([0-9]+)$")
if !listenRe.MatchString(config.Listen) {
config.hostname = config.Listen
config.Listen += ":1965"
} else {
config.hostname = split[0]
config.port, err = strconv.Atoi(split[1])
config.hostname = listenRe.ReplaceAllString(config.Listen, "$1")
config.port, err = strconv.Atoi(listenRe.ReplaceAllString(config.Listen, "$2"))
if err != nil {
log.Fatalf("invalid port specified: %s", err)
}
@ -147,6 +153,8 @@ func readconfig(configPath string) error {
config.Types[".gemini"] = geminiType
}
gmitohtml.Config.ConvertImages = config.ShowImages
defaultHost := config.Hosts["default"]
delete(config.Hosts, "default")
@ -210,6 +218,20 @@ func readconfig(configPath string) error {
}
host.cert = &cert
// Custom CSS stylesheets are precached in customCSS and used on HTTPS requests.
if host.StyleSheet != "" {
_, err := os.Stat(host.StyleSheet)
if os.IsNotExist(err) {
log.Printf("error: stylesheet '%s' not found", host.StyleSheet)
} else {
host.css, err = ioutil.ReadFile(host.StyleSheet)
if err != nil {
host.css = nil
log.Printf("error: failed to read stylesheet %s: %s", host.StyleSheet, err)
}
}
}
for _, serve := range host.Paths {
if serve.Path == "" {
log.Fatal("a path must be specified in each serve entry")

6
go.mod
View File

@ -3,10 +3,10 @@ module code.rocketnine.space/tslocum/twins
go 1.16
require (
code.rocketnine.space/tslocum/gmitohtml v1.0.4
code.rocketnine.space/tslocum/gmitohtml v1.0.5
github.com/h2non/filetype v1.1.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/yookoala/gofast v0.6.0
golang.org/x/tools v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
golang.org/x/tools v0.1.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)

26
go.sum
View File

@ -1,5 +1,6 @@
code.rocketnine.space/tslocum/gmitohtml v1.0.4 h1:EZsXfnP1sZx85PYNQb9XWFuF4WxSgeeTN3vxPsdGKyY=
code.rocketnine.space/tslocum/gmitohtml v1.0.4/go.mod h1:1FWEBFtkgRZgRE7ktApyFe29nm3MCNANvH/Mp8hyG54=
code.rocketnine.space/tslocum/ez v0.0.0-20210506054357-569018bd037a/go.mod h1:SQrM+bQ4eZdyAVTxuF2BNnyAnojHP6Kcmm2vMszoFWw=
code.rocketnine.space/tslocum/gmitohtml v1.0.5 h1:cc4MdgoGS7zBahUJovz6qDLa94eZpc2ncby4gxmckNc=
code.rocketnine.space/tslocum/gmitohtml v1.0.5/go.mod h1:QOy0pJQltuKQQDupfkmtN1DOlrUBCg55xFaQwoKQwlM=
github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -14,37 +15,40 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9
github.com/yookoala/gofast v0.6.0 h1:E5x2acfUD7GkzCf8bmIMwnV10VxDy5tUCHc5LGhluwc=
github.com/yookoala/gofast v0.6.0/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20210105161348-2e78108cf5f8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -14,12 +14,14 @@ func init() {
}
var quiet bool
var debug bool
func main() {
log.SetFlags(0)
configFile := flag.String("config", "", "path to configuration file")
flag.BoolVar(&quiet, "quiet", false, "do not print access log")
flag.BoolVar(&debug, "debug", false, "print debug information")
flag.Parse()
if *configFile == "" {

View File

@ -30,16 +30,6 @@ func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
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)
if r.Method == "HEAD" {
return status, 0, ""
}
w.Write(cssBytes)
return status, int64(len(cssBytes)), ""
}
pathBytes := []byte(r.URL.Path)
@ -49,6 +39,23 @@ func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
}
if host, ok := config.Hosts[r.URL.Hostname()]; ok {
if strings.HasSuffix(r.URL.Path, "/assets/style.css") {
status := http.StatusOK
w.Header().Set("Content-Type", cssType)
w.WriteHeader(status)
if r.Method == "HEAD" {
return status, 0, ""
}
if host.css != nil {
w.Write(host.css)
} else {
w.Write(cssBytes)
}
return status, int64(len(cssBytes)), ""
}
for _, serve := range host.Paths {
matchedRegexp := serve.r != nil && serve.r.Match(pathBytes)
matchedPrefix := serve.r == nil && strings.HasPrefix(r.URL.Path, serve.Path)
@ -176,11 +183,15 @@ func serveHTTPS(w http.ResponseWriter, r *http.Request) (int, int64, string) {
fileExt := strings.ToLower(filepath.Ext(filePath))
if fileExt == ".gmi" || fileExt == ".gemini" {
result = gmitohtml.Convert([]byte(data), r.URL.String())
} else if fileExt == ".htm" || fileExt == ".html" {
result = data
} else {
result = data
contentType = plainType
if fileExt == ".htm" || fileExt == ".html" {
// HTML content type already set
} else if customType := config.Types[filepath.Ext(filePath)]; customType != "" {
contentType = customType
} else {
contentType = plainType
}
}
status := http.StatusOK

View File

@ -351,6 +351,9 @@ func handleConn(c *tls.Conn) {
if err == io.EOF {
break
} else if err != nil || n != 1 {
if debug {
log.Printf("error: failed to read client: %s", err)
}
return
}