Index page and post pages ready
This commit is contained in:
4
vendor/github.com/a-h/gemini/.gitignore
generated
vendored
Normal file
4
vendor/github.com/a-h/gemini/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
cmd/gemini
|
||||
server.crt
|
||||
server.key
|
||||
41
vendor/github.com/a-h/gemini/.goreleaser.yml
generated
vendored
Normal file
41
vendor/github.com/a-h/gemini/.goreleaser.yml
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See documentation at http://goreleaser.com
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
dir: cmd
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
- amd64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
dockers:
|
||||
- image_templates:
|
||||
- adrianhesketh/gemini
|
||||
extra_files:
|
||||
- serve-gemini.sh
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
10
vendor/github.com/a-h/gemini/Dockerfile
generated
vendored
Normal file
10
vendor/github.com/a-h/gemini/Dockerfile
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
COPY gemini /gemini
|
||||
COPY serve-gemini.sh /
|
||||
|
||||
RUN chmod +x ./serve-gemini.sh
|
||||
|
||||
ENTRYPOINT ["./serve-gemini.sh"]
|
||||
21
vendor/github.com/a-h/gemini/LICENSE
generated
vendored
Normal file
21
vendor/github.com/a-h/gemini/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Adrian Hesketh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
19
vendor/github.com/a-h/gemini/Makefile
generated
vendored
Normal file
19
vendor/github.com/a-h/gemini/Makefile
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
build:
|
||||
go build -o gemini ./cmd/main.go
|
||||
|
||||
build-docker:
|
||||
docker build . -t adrianhesketh/gemini
|
||||
|
||||
build-snapshot:
|
||||
goreleaser build --snapshot --rm-dist
|
||||
|
||||
serve-local-tests:
|
||||
@echo add '127.0.0.1 a-h.gemini' to your /etc/hosts file
|
||||
openssl ecparam -genkey -name secp384r1 -out server.key
|
||||
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -subj "/C=/ST=/L=/O=/OU=/CN=a-h.gemini"
|
||||
go run ./cmd/main.go serve --domain=a-h.gemini --certFile=server.crt --keyFile=server.key --path=./tests
|
||||
|
||||
release:
|
||||
if [ "${GITHUB_TOKEN}" == "" ]; then echo "Set the GITHUB_TOKEN environment variable"; fi
|
||||
./push-tag.sh
|
||||
goreleaser --rm-dist
|
||||
206
vendor/github.com/a-h/gemini/README.md
generated
vendored
Normal file
206
vendor/github.com/a-h/gemini/README.md
generated
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
# Gemini
|
||||
|
||||
Applications and libraries for building applications on Gemini (see https://gemini.circumlunar.space/).
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
### Run a server
|
||||
|
||||
```sh
|
||||
gemini serve --domain=example.com --certFile=a.crt --keyFile=a.key --path=.
|
||||
```
|
||||
|
||||
### Request content
|
||||
|
||||
curl for Gemini.
|
||||
|
||||
```sh
|
||||
gemini request --insecure --verbose gemini://example.com/pass
|
||||
```
|
||||
|
||||
## Gemini Server Docker image
|
||||
|
||||
### Run a server with Docker
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
-v /path_to_your_cert_files:/certs \
|
||||
-e PORT=1965 \
|
||||
-e DOMAIN=localhost \
|
||||
-v /path_to_your_content:/content \
|
||||
-p 1965:1965 \
|
||||
adrianhesketh/gemini:latest
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
Check out https://github.com/a-h/gemini/releases for the latest version of the `gemini` command line tool to run locally, or use Docker:
|
||||
|
||||
```sh
|
||||
# Create a server certificate.
|
||||
openssl ecparam -genkey -name secp384r1 -out server.key
|
||||
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
# Make a Gemini file.
|
||||
mkdir content
|
||||
echo "# Hello, World!" > content/index.gmi
|
||||
# Run the container.
|
||||
docker pull adrianhesketh/gemini:latest
|
||||
docker run -v `pwd`:/certs -e PORT=1965 -e DOMAIN=localhost -v `pwd`/content:/content -p 1965:1965 adrianhesketh/gemini:latest
|
||||
```
|
||||
|
||||
## Libraries
|
||||
|
||||
### Serve
|
||||
|
||||
Use `gemini.Server` / `gemini.ListenAndServe` to build your own custom servers.
|
||||
|
||||
Supports hosting multiple Gemini servers on a single IP address.
|
||||
|
||||
These are used to build a Gemini application that supports dynamic content.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/a-h/gemini"
|
||||
"github.com/a-h/gemini/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create the handlers for a domain (a.gemini).
|
||||
okHandler := gemini.HandlerFunc(func(w gemini.ResponseWriter, r *gemini.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
helloHandler := gemini.HandlerFunc(func(w gemini.ResponseWriter, r *gemini.Request) {
|
||||
w.Write([]byte("# Hello, user!\n"))
|
||||
if r.Certificate.ID == "" {
|
||||
w.Write([]byte("You're not authenticated"))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf("Certificate: %v\n", r.Certificate.ID)))
|
||||
})
|
||||
|
||||
// Create a router for gemini://a.gemini/require_cert and gemini://a.gemini/public
|
||||
routerA := mux.NewMux()
|
||||
// Let's make /require_cert require the client to be authenticated.
|
||||
routerA.AddRoute("/require_cert", gemini.RequireCertificateHandler(helloHandler, nil))
|
||||
routerA.AddRoute("/public", okHandler)
|
||||
|
||||
// Create a file system handler gemini://b.gemini/{path}
|
||||
handlerB := gemini.FileSystemHandler(gemini.Dir("./content"))
|
||||
|
||||
// Set up the domain handlers.
|
||||
ctx := context.Background()
|
||||
a, err := gemini.NewDomainHandler("a.gemini", "a.crt", "a.key", routerA)
|
||||
if err != nil {
|
||||
log.Fatal("error creating domain handler A:", err)
|
||||
}
|
||||
b, err := gemini.NewDomainHandler("b.gemini", "b.crt", "b.key", handlerB)
|
||||
if err != nil {
|
||||
log.Fatal("error creating domain handler B:", err)
|
||||
}
|
||||
|
||||
// Start the server for two domains (a.gemini / b.gemini).
|
||||
err = gemini.ListenAndServe(ctx, ":1965", a, b)
|
||||
if err != nil {
|
||||
log.Fatal("error:", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Route
|
||||
|
||||
Use `github.com/a-h/gemini/mux` to provide routing between Gemini handlers and extract variables from URL paths.
|
||||
|
||||
### Built-in utility handlers
|
||||
|
||||
* `RequireCertificateHandler` a handler that ensures that users present certificates.
|
||||
* `FileSystemHandler` to support hosting static content.
|
||||
|
||||
|
||||
### Gemini client
|
||||
|
||||
```go
|
||||
client := gemini.NewClient()
|
||||
|
||||
// Make a request to the server without accepting its certificate.
|
||||
r, certificates, authenticated, ok, err := client.Request("gemini://a.gemini/require_cert")
|
||||
if err != nil {
|
||||
log.Printf("Request failed: %v", err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
Configure allowed server certificates for trust-on-first-use certificate support:
|
||||
|
||||
```
|
||||
client.AddAlllowedCertificateForHost("a.gemini", "3082016c3081f3020900d4c7c9907518eb61300a06082a8648ce3d0403023020310b30090603550406130267623111300f06035504030c08612e67656d696e69301e170d3230303832303139303330335a170d3330303831383139303330335a3020310b30090603550406130267623111300f06035504030c08612e67656d696e693076301006072a8648ce3d020106052b8104002203620004ae5cabe01f708d8f9423725df49601e1a033a1b51eb73cd3a8a9853011346127cbfedb57c4bd14ad6000ccb2f748d32b2a2b817b1860781d937e7666680874876fb4a9a91c44e2cf8c9804d40f6e7122f6c92a1884b62bd9f0749cca4e12cfa8300a06082a8648ce3d0403020368003065023100ae447eb9455e9ca1f02f013390d2c4029a7f29732cf6e29787b53b6435904d622f47f3b1fbffe60a284dbd4cddd6ef580230518dcb0355d5c3d880357128972c630ca90a915f1eb417a7ea0e4518a72dfc8a76c9b50c51d56f6a6835c4dfa989b72be3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### test
|
||||
|
||||
Test the project.
|
||||
|
||||
```sh
|
||||
go test ./... -short
|
||||
```
|
||||
|
||||
### test-integration
|
||||
|
||||
Integration test the project.
|
||||
|
||||
```sh
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### build
|
||||
|
||||
Build the CLI.
|
||||
|
||||
```sh
|
||||
go build -o gemini ./cmd/main.go
|
||||
```
|
||||
|
||||
### build-docker
|
||||
|
||||
Build the Docker image.
|
||||
|
||||
```sh
|
||||
docker build . -t adrianhesketh/gemini
|
||||
```
|
||||
|
||||
### build-snapshot
|
||||
|
||||
Build a snapshot release using goreleaser.
|
||||
|
||||
```sh
|
||||
goreleaser build --snapshot --rm-dist
|
||||
```
|
||||
|
||||
### serve-local-tests
|
||||
|
||||
Run a local Gemini server.
|
||||
|
||||
```sh
|
||||
echo add '127.0.0.1 a-h.gemini' to your /etc/hosts file
|
||||
openssl ecparam -genkey -name secp384r1 -out server.key
|
||||
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -subj "/C=/ST=/L=/O=/OU=/CN=a-h.gemini"
|
||||
go run ./cmd/main.go serve --domain=a-h.gemini --certFile=server.crt --keyFile=server.key --path=./tests
|
||||
```
|
||||
|
||||
### release
|
||||
|
||||
Push a release to Github.
|
||||
|
||||
```
|
||||
if [ "${GITHUB_TOKEN}" == "" ]; then echo "Set the GITHUB_TOKEN environment variable"; fi
|
||||
./push-tag.sh
|
||||
goreleaser --rm-dist
|
||||
```
|
||||
292
vendor/github.com/a-h/gemini/client.go
generated
vendored
Normal file
292
vendor/github.com/a-h/gemini/client.go
generated
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Response from the Gemini server.
|
||||
type Response struct {
|
||||
Header *Header
|
||||
Body io.ReadCloser
|
||||
}
|
||||
|
||||
// NewResponse parses the server response.
|
||||
func NewResponse(r io.ReadCloser) (resp *Response, err error) {
|
||||
resp = &Response{
|
||||
Body: r,
|
||||
}
|
||||
h, err := readHeader(r)
|
||||
resp.Header = &h
|
||||
return
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
Code Code
|
||||
Meta string
|
||||
}
|
||||
|
||||
// ErrInvalidStatus is returned if the Gemini request did not match the expected format.
|
||||
var ErrInvalidStatus = errors.New("gemini: server status did not match the expected format")
|
||||
|
||||
// ErrInvalidCode is returned if the Gemini server returns an invalid code.
|
||||
var ErrInvalidCode = errors.New("gemini: invalid code")
|
||||
|
||||
// ErrInvalidMeta is returned if the Gemini server returns an invalid meta value.
|
||||
var ErrInvalidMeta = errors.New("gemini: invalid meta")
|
||||
|
||||
// ErrCrLfNotFoundWithinMaxLength is returned if the Gemini server returns an invalid response.
|
||||
var ErrCrLfNotFoundWithinMaxLength = errors.New("gemini: invalid header - CRLF not found within maximum length")
|
||||
|
||||
func readHeader(r io.Reader) (h Header, err error) {
|
||||
// Read <STATUS><SPACE><META><CR><LF>
|
||||
statusLine, ok, err := readUntilCrLf(r, 1029)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gemini: failed to read status line: %v", err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
err = ErrCrLfNotFoundWithinMaxLength
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimRight(string(statusLine), "\r\n"), " ", 2)
|
||||
if len(parts) != 1 && len(parts) != 2 {
|
||||
err = ErrInvalidStatus
|
||||
return
|
||||
}
|
||||
h.Code = Code(parts[0])
|
||||
if !isValidCode(h.Code) {
|
||||
err = ErrInvalidCode
|
||||
return
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
h.Meta = parts[1]
|
||||
if !isValidMeta(h.Meta) {
|
||||
err = ErrInvalidMeta
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func readUntilCrLf(src io.Reader, maxLength int) (output []byte, ok bool, err error) {
|
||||
var previousIsCr bool
|
||||
buffer := make([]byte, 1)
|
||||
for i := 0; i < maxLength; i++ {
|
||||
_, err = src.Read(buffer)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
current := buffer[0]
|
||||
if current == '\n' {
|
||||
if previousIsCr {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
previousIsCr = current == '\r'
|
||||
output = append(output, buffer[0])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var validStart map[byte]bool = map[byte]bool{
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
}
|
||||
|
||||
func isValidCode(code Code) bool {
|
||||
if len(code) == 0 {
|
||||
return false
|
||||
}
|
||||
return validStart[code[0]]
|
||||
}
|
||||
|
||||
func isValidMeta(m string) bool {
|
||||
return len(m) <= 1024
|
||||
}
|
||||
|
||||
// NewClient creates a new gemini client.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
prefixToCertificate: make(map[string]tls.Certificate),
|
||||
domainToAllowedCertificateHash: make(map[string]map[string]interface{}),
|
||||
WriteTimeout: time.Second * 5,
|
||||
ReadTimeout: time.Second * 5,
|
||||
}
|
||||
}
|
||||
|
||||
// Client for Gemini requests.
|
||||
type Client struct {
|
||||
// prefixToCertificate maps URL prefixes to certificates.
|
||||
// Load a keypair from disk with tls.LoadX509KeyPair("client.pem", "client.key")
|
||||
// If a certificate is not required, use &Client{}.
|
||||
prefixToCertificate map[string]tls.Certificate
|
||||
// domainToAllowedCertificateHash is used to validate the remote server.
|
||||
domainToAllowedCertificateHash map[string]map[string]interface{}
|
||||
// Insecure mode does not check the hash of remote certificates.
|
||||
Insecure bool
|
||||
WriteTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
}
|
||||
|
||||
// AddClientCertificate adds a certificate to use when the URL prefix is encountered.
|
||||
func (client *Client) AddClientCertificate(prefix string, cert tls.Certificate) {
|
||||
client.prefixToCertificate[prefix] = cert
|
||||
}
|
||||
|
||||
// AddServerCertificate allows the client to connect to a domain based on its hash.
|
||||
func (client *Client) AddServerCertificate(host, certificateHash string) {
|
||||
host = strings.ToLower(host)
|
||||
if m := client.domainToAllowedCertificateHash[host]; m == nil {
|
||||
client.domainToAllowedCertificateHash[host] = make(map[string]interface{})
|
||||
}
|
||||
client.domainToAllowedCertificateHash[host][certificateHash] = struct{}{}
|
||||
}
|
||||
|
||||
// Request a response from a given Gemini URL.
|
||||
func (client *Client) Request(ctx context.Context, u string) (resp *Response, certificates []string, authenticated, ok bool, err error) {
|
||||
uu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return client.RequestURL(ctx, uu)
|
||||
}
|
||||
|
||||
// GetCertificate returns a certificate to use for the given URL, if one exists.
|
||||
func (client *Client) GetCertificate(u *url.URL) (cert tls.Certificate, ok bool) {
|
||||
for k, v := range client.prefixToCertificate {
|
||||
if strings.HasPrefix(u.String(), k) {
|
||||
cert = v
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RequestNoTLS carries out a request without TLS enabled.
|
||||
func (client *Client) RequestNoTLS(ctx context.Context, u *url.URL) (resp *Response, err error) {
|
||||
dialer := net.Dialer{
|
||||
Timeout: client.ReadTimeout,
|
||||
}
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = "1965"
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", u.Hostname()+":"+port)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gemini: error connecting: %w", err)
|
||||
return
|
||||
}
|
||||
return client.RequestConn(ctx, conn, u)
|
||||
}
|
||||
|
||||
// RequestURL requests a response from a parsed URL.
|
||||
// ok returns true if a matching server certificate is found (i.e. the server is OK).
|
||||
func (client *Client) RequestURL(ctx context.Context, u *url.URL) (resp *Response, certificates []string, authenticated, ok bool, err error) {
|
||||
tlsDialer := tls.Dialer{
|
||||
NetDialer: &net.Dialer{
|
||||
Timeout: client.ReadTimeout,
|
||||
},
|
||||
Config: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
if cert, ok := client.GetCertificate(u); ok {
|
||||
tlsDialer.Config.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
port = "1965"
|
||||
}
|
||||
cn, err := tlsDialer.DialContext(ctx, "tcp", u.Hostname()+":"+port)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gemini: error connecting: %w", err)
|
||||
return
|
||||
}
|
||||
conn := cn.(*tls.Conn)
|
||||
allowedHashesForDomain := client.domainToAllowedCertificateHash[strings.ToLower(u.Host)]
|
||||
ok = false
|
||||
for _, cert := range conn.ConnectionState().PeerCertificates {
|
||||
hash := base64.StdEncoding.EncodeToString(sha256.New().Sum(cert.Raw))
|
||||
certificates = append(certificates, hash)
|
||||
if _, ok = allowedHashesForDomain[hash]; ok {
|
||||
break
|
||||
}
|
||||
if time.Now().Before(cert.NotBefore) {
|
||||
err = fmt.Errorf("gemini: expired certificate")
|
||||
return
|
||||
}
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
err = fmt.Errorf("gemini: certificate not yet valid")
|
||||
return
|
||||
}
|
||||
}
|
||||
if !ok && !client.Insecure {
|
||||
return
|
||||
}
|
||||
authenticated = conn.ConnectionState().NegotiatedProtocolIsMutual
|
||||
resp, err = client.RequestConn(ctx, conn, u)
|
||||
return
|
||||
}
|
||||
|
||||
type readerCtx struct {
|
||||
ctx context.Context
|
||||
r io.ReadCloser
|
||||
}
|
||||
|
||||
func (r *readerCtx) Read(p []byte) (n int, err error) {
|
||||
if err := r.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.r.Read(p)
|
||||
}
|
||||
|
||||
func (r *readerCtx) Close() (err error) {
|
||||
return r.r.Close()
|
||||
}
|
||||
|
||||
func newReaderContext(ctx context.Context, r io.ReadCloser) io.ReadCloser {
|
||||
return &readerCtx{
|
||||
ctx: ctx,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
// RequestConn uses a given connection to make the request. This allows for insecure requests to be made.
|
||||
// net.Dial("tcp", "localhost:1965")
|
||||
func (client *Client) RequestConn(ctx context.Context, conn net.Conn, u *url.URL) (resp *Response, err error) {
|
||||
conn.SetWriteDeadline(time.Now().Add(client.WriteTimeout))
|
||||
_, err = conn.Write([]byte(u.String() + "\r\n"))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gemini: error writing request: %w", err)
|
||||
return
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(client.ReadTimeout))
|
||||
resp, err = NewResponse(newReaderContext(ctx, conn))
|
||||
return
|
||||
}
|
||||
|
||||
// Record a Gemini handler request in memory and return the response.
|
||||
func Record(r *Request, handler Handler) (resp *Response, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
w := NewWriter(buf)
|
||||
handler.ServeGemini(w, r)
|
||||
return NewResponse(ioutil.NopCloser(bytes.NewBuffer(buf.Bytes())))
|
||||
}
|
||||
128
vendor/github.com/a-h/gemini/filesystem.go
generated
vendored
Normal file
128
vendor/github.com/a-h/gemini/filesystem.go
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/gemini/log"
|
||||
)
|
||||
|
||||
type Dir string
|
||||
|
||||
// Open implements FileSystem using os.Open, opening files for reading rooted
|
||||
// and relative to the directory d.
|
||||
func (d Dir) Open(name string) (File, error) {
|
||||
dir := string(d)
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
|
||||
return os.Open(fullName)
|
||||
}
|
||||
|
||||
// A FileSystem implements access to a collection of named files.
|
||||
// The elements in a file path are separated by slash ('/', U+002F)
|
||||
// characters, regardless of host operating system convention.
|
||||
type FileSystem interface {
|
||||
Open(name string) (File, error)
|
||||
}
|
||||
|
||||
// A File is returned by a FileSystem's Open method and can be
|
||||
// served by the FileServer implementation.
|
||||
//
|
||||
// The methods should behave the same as those on an *os.File.
|
||||
type File interface {
|
||||
io.Closer
|
||||
io.Reader
|
||||
Readdir(count int) ([]os.FileInfo, error)
|
||||
Stat() (os.FileInfo, error)
|
||||
}
|
||||
|
||||
func DirectoryListingHandler(path string, f File) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
files, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
log.Warn("DirectoryListingHandler: readdir failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
|
||||
w.SetHeader(CodeTemporaryFailure, "readdir failed")
|
||||
return
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })
|
||||
w.SetHeader(CodeSuccess, DefaultMIMEType)
|
||||
fmt.Fprintf(w, "# Index of %s\n\n", path)
|
||||
fmt.Fprintln(w, "=> ../")
|
||||
for _, ff := range files {
|
||||
name := ff.Name()
|
||||
if ff.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
url := url.URL{Path: name}
|
||||
fmt.Fprintf(w, "=> %v\n", url.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FileContentHandler(name string, f File) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
mType := mime.TypeByExtension(path.Ext(name))
|
||||
if mType == "" {
|
||||
mType = DefaultMIMEType
|
||||
}
|
||||
w.SetHeader(CodeSuccess, mType)
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
log.Error("FileContentHandler: failed to write file", err, log.String("fileName", name))
|
||||
panic("error returning file contents")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FileSystemHandler(fs FileSystem) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
if strings.Contains(r.URL.Path, "..") {
|
||||
log.Warn("FileSystemHandler: possible directory traversal attack", log.String("path", r.URL.Path), log.String("url", r.URL.String()))
|
||||
BadRequest(w, r)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, "/") {
|
||||
r.URL.Path = "/" + r.URL.Path
|
||||
}
|
||||
f, err := fs.Open(r.URL.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
NotFoundHandler().ServeGemini(w, r)
|
||||
return
|
||||
}
|
||||
log.Warn("FileSystemHandler: file open failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
|
||||
w.SetHeader(CodeTemporaryFailure, "file open failed")
|
||||
return
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
log.Warn("FileSystemHandler: file stat failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
|
||||
w.SetHeader(CodeTemporaryFailure, "file stat failed")
|
||||
return
|
||||
}
|
||||
if stat.IsDir() {
|
||||
// Look for index.gmi first before listing contents.
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
RedirectPermanentHandler(r.URL.Path+"/").ServeGemini(w, r)
|
||||
return
|
||||
}
|
||||
index, err := fs.Open(r.URL.Path + "index.gmi")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
DirectoryListingHandler(r.URL.Path, f).ServeGemini(w, r)
|
||||
return
|
||||
}
|
||||
FileContentHandler("index.gmi", index).ServeGemini(w, r)
|
||||
return
|
||||
}
|
||||
FileContentHandler(stat.Name(), f).ServeGemini(w, r)
|
||||
})
|
||||
}
|
||||
98
vendor/github.com/a-h/gemini/handlers.go
generated
vendored
Normal file
98
vendor/github.com/a-h/gemini/handlers.go
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BadRequest responds with a 59 status.
|
||||
func BadRequest(w ResponseWriter, r *Request) {
|
||||
w.SetHeader(CodeBadRequest, "bad request")
|
||||
}
|
||||
|
||||
// BadRequestHandler creates a handler that returns a bad request code (59).
|
||||
func BadRequestHandler() Handler {
|
||||
return HandlerFunc(BadRequest)
|
||||
}
|
||||
|
||||
// NotFound responds with a 51 status.
|
||||
func NotFound(w ResponseWriter, r *Request) {
|
||||
w.SetHeader(CodeNotFound, "not found")
|
||||
}
|
||||
|
||||
// NotFoundHandler creates a handler that returns not found.
|
||||
func NotFoundHandler() Handler {
|
||||
return HandlerFunc(NotFound)
|
||||
}
|
||||
|
||||
// RedirectTemporaryHandler returns a temporary redirection.
|
||||
func RedirectTemporaryHandler(to string) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
w.SetHeader(CodeRedirect, to)
|
||||
})
|
||||
}
|
||||
|
||||
// RedirectPermanentHandler returns a handler which returns a permanent redirect.
|
||||
func RedirectPermanentHandler(to string) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
w.SetHeader(CodeRedirectPermanent, to)
|
||||
})
|
||||
}
|
||||
|
||||
// StripPrefixHandler strips a prefix from the incoming URL and passes the strippe URL to h.
|
||||
func StripPrefixHandler(prefix string, h Handler) Handler {
|
||||
if prefix == "" {
|
||||
return h
|
||||
}
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
|
||||
r2 := new(Request)
|
||||
*r2 = *r
|
||||
r2.URL = new(url.URL)
|
||||
*r2.URL = *r.URL
|
||||
r2.URL.Path = p
|
||||
h.ServeGemini(w, r2)
|
||||
return
|
||||
}
|
||||
NotFound(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RequireCertificateHandler returns a handler that enforces authentication on h.
|
||||
// authoriser can be set to limit which users can access h. If authoriser
|
||||
// is nil, authoriser is set to AuthoriserAllowAll which allows any authenticated
|
||||
// user to access the handler.
|
||||
func RequireCertificateHandler(h Handler, authoriser func(certID, certKey string) bool) Handler {
|
||||
if authoriser == nil {
|
||||
authoriser = AuthoriserAllowAll
|
||||
}
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
if r.Certificate.ID == "" {
|
||||
w.SetHeader(CodeClientCertificateRequired, "client certificate required")
|
||||
return
|
||||
}
|
||||
if !authoriser(r.Certificate.ID, r.Certificate.Key) {
|
||||
w.SetHeader(CodeClientCertificateNotAuthorised, "not authorised")
|
||||
return
|
||||
}
|
||||
h.ServeGemini(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RequireInputHandler returns a handler that enforces all incoming requests have a populated
|
||||
// querystring.
|
||||
// `prompt` is returned as response META if input is not provided.
|
||||
func RequireInputHandler(h Handler, prompt string) Handler {
|
||||
return HandlerFunc(func(w ResponseWriter, r *Request) {
|
||||
if r.URL.RawQuery == "" {
|
||||
w.SetHeader(CodeInput, prompt)
|
||||
return
|
||||
}
|
||||
h.ServeGemini(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// AuthoriserAllowAll allows any authenticated user to access the handler.
|
||||
func AuthoriserAllowAll(id, key string) bool {
|
||||
return true
|
||||
}
|
||||
82
vendor/github.com/a-h/gemini/log/log.go
generated
vendored
Normal file
82
vendor/github.com/a-h/gemini/log/log.go
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Field func() (k string, v interface{})
|
||||
|
||||
func String(name string, value string) Field {
|
||||
return func() (k string, v interface{}) {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
func Int(name string, value int) Field {
|
||||
return func() (k string, v interface{}) {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
func Int64(name string, value int64) Field {
|
||||
return func() (k string, v interface{}) {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
func Interface(name string, value interface{}) Field {
|
||||
return func() (k string, v interface{}) {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
|
||||
func Info(msg string, fields ...Field) {
|
||||
std.Write("INFO", msg, fields...)
|
||||
}
|
||||
|
||||
func Warn(msg string, fields ...Field) {
|
||||
std.Write("WARN", msg, fields...)
|
||||
}
|
||||
|
||||
func Error(msg string, err error, fields ...Field) {
|
||||
if err != nil {
|
||||
fields = append(fields, String("error", err.Error()))
|
||||
}
|
||||
std.Write("ERROR", msg, fields...)
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
stdout sync.Mutex
|
||||
stderr sync.Mutex
|
||||
enc json.Encoder
|
||||
newLine []byte
|
||||
}
|
||||
|
||||
func (s *Logger) Write(level string, msg string, fields ...Field) {
|
||||
entry := make(map[string]interface{})
|
||||
entry["time"] = time.Now().UTC().Format(time.RFC3339)
|
||||
entry["level"] = level
|
||||
entry["msg"] = msg
|
||||
for i := 0; i < len(fields); i++ {
|
||||
k, v := fields[i]()
|
||||
entry[k] = v
|
||||
}
|
||||
j, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
j = []byte(fmt.Sprintf("log.Write: failed to marshal entry '%+v': %v", entry, err))
|
||||
}
|
||||
if level == "ERROR" {
|
||||
s.stderr.Lock()
|
||||
defer s.stderr.Unlock()
|
||||
os.Stderr.Write(j)
|
||||
os.Stderr.Write(s.newLine)
|
||||
return
|
||||
}
|
||||
s.stdout.Lock()
|
||||
defer s.stdout.Unlock()
|
||||
os.Stdout.Write(j)
|
||||
os.Stderr.Write(s.newLine)
|
||||
}
|
||||
|
||||
var std = &Logger{newLine: []byte("\n")}
|
||||
76
vendor/github.com/a-h/gemini/mux/mux.go
generated
vendored
Normal file
76
vendor/github.com/a-h/gemini/mux/mux.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/gemini"
|
||||
)
|
||||
|
||||
// Mux routes Gemini requests to the appropriate handler.
|
||||
type Mux struct {
|
||||
RouteHandlers []*RouteHandler
|
||||
NotFoundHandler gemini.Handler
|
||||
}
|
||||
|
||||
// NewMux creates a new Mux for routing requests.
|
||||
func NewMux() *Mux {
|
||||
return &Mux{
|
||||
RouteHandlers: make([]*RouteHandler, 0),
|
||||
NotFoundHandler: gemini.NotFoundHandler(),
|
||||
}
|
||||
}
|
||||
|
||||
// AddRoute to the mux.
|
||||
func (m *Mux) AddRoute(pattern string, handler gemini.Handler) {
|
||||
rh := &RouteHandler{
|
||||
Route: NewRoute(pattern),
|
||||
Handler: handler,
|
||||
}
|
||||
m.RouteHandlers = append(m.RouteHandlers, rh)
|
||||
}
|
||||
|
||||
// RouteHandler is the Handler to use for a given Route.
|
||||
type RouteHandler struct {
|
||||
Route *Route
|
||||
Handler gemini.Handler
|
||||
}
|
||||
|
||||
// contextKey used to store the route handler in the request context.
|
||||
type contextKey string
|
||||
|
||||
// matchedRouteContextKey is the key stored in the context.
|
||||
const matchedRouteContextKey = contextKey("matchedRoute")
|
||||
|
||||
// MatchedRoute is provided in the context to Gemini handlers that use the router.
|
||||
type MatchedRoute struct {
|
||||
Pattern string
|
||||
PathVars map[string]string
|
||||
}
|
||||
|
||||
func (m *Mux) ServeGemini(w gemini.ResponseWriter, r *gemini.Request) {
|
||||
s := r.URL.Path
|
||||
s = strings.TrimSuffix(s, "/")
|
||||
s = strings.TrimPrefix(s, "/")
|
||||
segments := strings.Split(s, "/")
|
||||
|
||||
for _, rh := range m.RouteHandlers {
|
||||
v, ok := rh.Route.Match(segments)
|
||||
if ok {
|
||||
mr := MatchedRoute{
|
||||
Pattern: rh.Route.Pattern,
|
||||
PathVars: v,
|
||||
}
|
||||
r.Context = context.WithValue(r.Context, matchedRouteContextKey, mr)
|
||||
rh.Handler.ServeGemini(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
m.NotFoundHandler.ServeGemini(w, r)
|
||||
}
|
||||
|
||||
// GetMatchedRoute returns the route that was matched by the router, along with any path variables extracted from the URL.
|
||||
func GetMatchedRoute(ctx context.Context) (mr MatchedRoute, ok bool) {
|
||||
mr, ok = ctx.Value(matchedRouteContextKey).(MatchedRoute)
|
||||
return mr, ok
|
||||
}
|
||||
69
vendor/github.com/a-h/gemini/mux/route.go
generated
vendored
Normal file
69
vendor/github.com/a-h/gemini/mux/route.go
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Route is an array of segments.
|
||||
type Route struct {
|
||||
Pattern string
|
||||
Segments []*Segment
|
||||
}
|
||||
|
||||
// NewRoute creates a route based on a pattern, e.g /users/{userid}.
|
||||
func NewRoute(pattern string) *Route {
|
||||
var r Route
|
||||
r.Pattern = pattern
|
||||
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
|
||||
for _, seg := range strings.Split(pattern, "/") {
|
||||
ps := &Segment{
|
||||
Name: seg,
|
||||
}
|
||||
if seg == "*" {
|
||||
ps.IsWildcard = true
|
||||
}
|
||||
if strings.HasPrefix(seg, "{") && strings.HasSuffix(seg, "}") {
|
||||
ps.IsVariable = true
|
||||
ps.Name = strings.TrimSuffix(strings.TrimPrefix(seg, "{"), "}")
|
||||
}
|
||||
r.Segments = append(r.Segments, ps)
|
||||
}
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
// Match returns whether the route was matched, and extracts variables.
|
||||
func (r Route) Match(segments []string) (vars map[string]string, ok bool) {
|
||||
vars = make(map[string]string)
|
||||
var wildcard bool
|
||||
for i := 0; i < len(r.Segments); i++ {
|
||||
routeSegment := r.Segments[len(r.Segments)-1-i]
|
||||
inputSegmentIndex := len(segments) - 1 - i
|
||||
var inputSegment string
|
||||
if inputSegmentIndex > -1 {
|
||||
inputSegment = segments[inputSegmentIndex]
|
||||
}
|
||||
name, capture, wildcardMatch, matches := routeSegment.Match(inputSegment)
|
||||
if matches {
|
||||
if wildcardMatch {
|
||||
wildcard = true
|
||||
} else {
|
||||
wildcard = false
|
||||
}
|
||||
}
|
||||
if wildcard {
|
||||
matches = true
|
||||
}
|
||||
if !matches {
|
||||
return
|
||||
}
|
||||
if capture {
|
||||
vars[name] = inputSegment
|
||||
}
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
40
vendor/github.com/a-h/gemini/mux/segment.go
generated
vendored
Normal file
40
vendor/github.com/a-h/gemini/mux/segment.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Segment is a path segment, e.g. in /users/{userid}/ there are two segments,
|
||||
// "users" and "{userid}". "{userid}" is a variable and will be captured.
|
||||
type Segment struct {
|
||||
Name string
|
||||
IsVariable bool
|
||||
IsWildcard bool
|
||||
}
|
||||
|
||||
// String pretty prints the segment, for debugging.
|
||||
func (ps *Segment) String() string {
|
||||
return fmt.Sprintf("{ Name: %v, IsVariable: %v, IsWildcard: %v }",
|
||||
ps.Name, ps.IsVariable, ps.IsWildcard)
|
||||
}
|
||||
|
||||
// Match on the string path segment.
|
||||
func (ps *Segment) Match(s string) (name string, capture bool, wildcard bool, matches bool) {
|
||||
if ps.IsWildcard {
|
||||
wildcard = true
|
||||
matches = true
|
||||
return
|
||||
}
|
||||
if ps.IsVariable {
|
||||
name = ps.Name
|
||||
capture = true
|
||||
matches = true
|
||||
return
|
||||
}
|
||||
if strings.EqualFold(s, ps.Name) {
|
||||
matches = true
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
4
vendor/github.com/a-h/gemini/push-tag.sh
generated
vendored
Normal file
4
vendor/github.com/a-h/gemini/push-tag.sh
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export VERSION=`git rev-list --count HEAD`;
|
||||
echo Adding git tag with version v0.0.${VERSION};
|
||||
git tag v0.0.${VERSION};
|
||||
git push origin v0.0.${VERSION};
|
||||
60
vendor/github.com/a-h/gemini/serve-gemini.sh
generated
vendored
Normal file
60
vendor/github.com/a-h/gemini/serve-gemini.sh
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env sh
|
||||
pid=0
|
||||
|
||||
# Respond to ctrl-c as per https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86
|
||||
|
||||
# SIGTERM-handler
|
||||
term_handler() {
|
||||
if [ $pid -ne 0 ]; then
|
||||
kill -SIGTERM "$pid"
|
||||
wait "$pid"
|
||||
fi
|
||||
exit 143; # 128 + 15 -- SIGTERM
|
||||
}
|
||||
|
||||
# setup handlers
|
||||
# on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler
|
||||
trap 'kill ${!}; term_handler' SIGTERM
|
||||
|
||||
# Configure defaults.
|
||||
if [ "$PORT" = "" ];
|
||||
then
|
||||
export PORT=1965;
|
||||
fi
|
||||
if [ "$DOMAIN" = "" ];
|
||||
then
|
||||
export DOMAIN=localhost;
|
||||
fi
|
||||
if [ ! -d /certs ];
|
||||
then
|
||||
echo "Docker usage:"
|
||||
echo ""
|
||||
echo "Create server certificates with the domain set as the common name (all other fields can be left default):"
|
||||
echo " openssl ecparam -genkey -name secp384r1 -out server.key"
|
||||
echo " openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650"
|
||||
echo ""
|
||||
echo "Then run Docker container:"
|
||||
echo ""
|
||||
echo " docker run -v /path_to_your_cert_files:/certs -e PORT=1965 -e DOMAIN=localhost -v /path_to_your_content:/content -p 1965:1965 adrianhesketh/gemini:latest"
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
if [ ! -e /certs/server.crt ];
|
||||
then
|
||||
echo "server.crt file not found, are your certs named correctly?"
|
||||
fi
|
||||
if [ ! -e /certs/server.key ];
|
||||
then
|
||||
echo "server.key file not found, are your certs named correctly?"
|
||||
fi
|
||||
|
||||
# run application
|
||||
# Run server.
|
||||
./gemini serve --path=/content --certFile=/certs/server.crt --keyFile=/certs/server.key --port=$PORT --domain=$DOMAIN &
|
||||
pid="$!"
|
||||
|
||||
# wait forever
|
||||
while true
|
||||
do
|
||||
tail -f /dev/null & wait ${!}
|
||||
done
|
||||
426
vendor/github.com/a-h/gemini/server.go
generated
vendored
Normal file
426
vendor/github.com/a-h/gemini/server.go
generated
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/gemini/log"
|
||||
)
|
||||
|
||||
// Handler of Gemini content.
|
||||
type Handler interface {
|
||||
ServeGemini(w ResponseWriter, r *Request)
|
||||
}
|
||||
|
||||
// HandlerFunc handles a Gemini request and returns a response.
|
||||
type HandlerFunc func(ResponseWriter, *Request)
|
||||
|
||||
// ServeGemini implements the Handler interface.
|
||||
func (f HandlerFunc) ServeGemini(w ResponseWriter, r *Request) {
|
||||
f(w, r)
|
||||
}
|
||||
|
||||
// DefaultMIMEType for Gemini responses.
|
||||
const DefaultMIMEType = "text/gemini; charset=utf-8"
|
||||
|
||||
// Request from the client. A Gemini request contains only the
|
||||
// URL, the Certificates field is populated by the TLS certificates
|
||||
// presented by the client.
|
||||
type Request struct {
|
||||
Context context.Context
|
||||
URL *url.URL
|
||||
Certificate Certificate
|
||||
}
|
||||
|
||||
// Certificate information provided to the server by the client.
|
||||
type Certificate struct {
|
||||
// ID is the base64-encoded SHA256 hash of the key.
|
||||
ID string
|
||||
// Key is the user public key in PKIX, ASN.1 DER form.
|
||||
Key string
|
||||
// Error is an error message related to any failures in handling the client certificate.
|
||||
Error string
|
||||
}
|
||||
|
||||
// ResponseWriter used by handlers to send a response to the client.
|
||||
type ResponseWriter interface {
|
||||
io.Writer
|
||||
SetHeader(code Code, meta string) error
|
||||
}
|
||||
|
||||
// Code returned as part of the Gemini response (see https://gemini.circumlunar.space/docs/specification.html).
|
||||
type Code string
|
||||
|
||||
const (
|
||||
CodeInput Code = "10"
|
||||
CodeInputSensitive = "11"
|
||||
CodeSuccess = "20"
|
||||
CodeRedirect = "30"
|
||||
CodeRedirectTemporary = CodeRedirect
|
||||
CodeRedirectPermanent = "31"
|
||||
CodeTemporaryFailure = "40"
|
||||
CodeServerUnavailable = "41"
|
||||
CodeCGIError = "42"
|
||||
CodeProxyError = "43"
|
||||
CodeSlowDown = "44"
|
||||
CodePermanentFailure = "50"
|
||||
CodeNotFound = "51"
|
||||
CodeGone = "52"
|
||||
CodeProxyRequestRefused = "53"
|
||||
CodeBadRequest = "59"
|
||||
CodeClientCertificateRequired = "60"
|
||||
CodeClientCertificateNotAuthorised = "61"
|
||||
CodeClientCertificateNotValid = "62"
|
||||
)
|
||||
|
||||
// IsErrorCode returns true if the code is invalid, or starts with 4, 5 or 6.
|
||||
func IsErrorCode(code Code) bool {
|
||||
if !isValidCode(code) || len(code) != 2 {
|
||||
return false
|
||||
}
|
||||
return code[0] == '4' || code[0] == '5' || code[0] == '6'
|
||||
}
|
||||
|
||||
// NewServer creates a new Gemini server.
|
||||
// addr is in the form "<optional_ip>:<port>", e.g. ":1965". If left empty, it will default to ":1965".
|
||||
// domainToHandler is a map of the server name (domain) to the certificate key pair and the Gemini handler used to serve content.
|
||||
func NewServer(ctx context.Context, addr string, domainToHandler map[string]*DomainHandler) *Server {
|
||||
for k, v := range domainToHandler {
|
||||
domainToHandler[strings.ToLower(k)] = v
|
||||
}
|
||||
return &Server{
|
||||
Context: ctx,
|
||||
Addr: addr,
|
||||
DomainToHandler: domainToHandler,
|
||||
ReadTimeout: time.Second * 5,
|
||||
WriteTimeout: time.Second * 10,
|
||||
HandlerTimeout: time.Second * 30,
|
||||
}
|
||||
}
|
||||
|
||||
// Server hosts Gemini content.
|
||||
type Server struct {
|
||||
Context context.Context
|
||||
Addr string
|
||||
Insecure bool
|
||||
DomainToHandler map[string]*DomainHandler
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
HandlerTimeout time.Duration
|
||||
}
|
||||
|
||||
// Set the server listening on the specified port.
|
||||
func (srv *Server) ListenAndServe() error {
|
||||
// Don't start if the server is already closing down.
|
||||
if srv.Context.Err() != nil {
|
||||
return ErrServerClosed
|
||||
}
|
||||
addr := srv.Addr
|
||||
if addr == "" {
|
||||
addr = ":1965"
|
||||
}
|
||||
log.Info("gemini: starting", log.String("addr", addr))
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ln.Close()
|
||||
if srv.Insecure {
|
||||
err = srv.serveInsecure(ln)
|
||||
if err != nil {
|
||||
log.Error("gemini: serveInsecure failure", err, log.String("addr", addr))
|
||||
}
|
||||
} else {
|
||||
err = srv.serveTLS(ln)
|
||||
if err != nil {
|
||||
log.Error("gemini: serveTLS failure", err, log.String("addr", addr))
|
||||
}
|
||||
}
|
||||
log.Info("gemini: stopped")
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrServerClosed is returned when a server is attempted to start up when it's already shutting down.
|
||||
var ErrServerClosed = errors.New("gemini: server closed")
|
||||
|
||||
func (srv *Server) serveInsecure(l net.Listener) (err error) {
|
||||
if len(srv.DomainToHandler) > 1 {
|
||||
return fmt.Errorf("gemini: cannot start insecure mode for more than one domain")
|
||||
}
|
||||
var handler *DomainHandler
|
||||
for _, handler = range srv.DomainToHandler {
|
||||
break
|
||||
}
|
||||
for {
|
||||
if err = srv.Context.Err(); err != nil {
|
||||
log.Error("gemini: context caused shutdown", err)
|
||||
return err
|
||||
}
|
||||
rw, err := l.Accept()
|
||||
if err != nil {
|
||||
log.Error("gemini: insecure listener error", err)
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
defer rw.Close()
|
||||
srv.handle(handler, Certificate{}, rw)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) serveTLS(l net.Listener) (err error) {
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
InsecureSkipVerify: true,
|
||||
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
dh, ok := srv.DomainToHandler[strings.ToLower(hello.ServerName)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("gemini: certificate not found for %q", hello.ServerName)
|
||||
}
|
||||
return &dh.KeyPair, nil
|
||||
},
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsListener := tls.NewListener(l, config)
|
||||
for {
|
||||
if err = srv.Context.Err(); err != nil {
|
||||
log.Error("gemini: context caused shutdown", err)
|
||||
return err
|
||||
}
|
||||
conn, err := tlsListener.Accept()
|
||||
if err != nil {
|
||||
log.Error("gemini: tls listener error", err)
|
||||
continue
|
||||
}
|
||||
tlsConn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
panic("gemini: tls.Listener did not return TLS connection")
|
||||
}
|
||||
go srv.handleTLS(tlsConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) handleTLS(conn *tls.Conn) {
|
||||
defer conn.Close()
|
||||
if err := conn.Handshake(); err != nil {
|
||||
log.Info("gemini: failed TLS handshake", log.String("remote", conn.RemoteAddr().String()), log.String("reason", err.Error()))
|
||||
return
|
||||
}
|
||||
var certificate Certificate
|
||||
peerCerts := conn.ConnectionState().PeerCertificates
|
||||
if len(peerCerts) > 0 {
|
||||
now := time.Now()
|
||||
cert := peerCerts[0]
|
||||
certificate.ID = base64.StdEncoding.EncodeToString(sha256.New().Sum(cert.Raw))
|
||||
certificate.Key = string(cert.Raw)
|
||||
if now.Before(cert.NotBefore) {
|
||||
certificate.Error = "certificate not yet valid"
|
||||
}
|
||||
if now.After(cert.NotAfter) {
|
||||
certificate.Error = "certificate expired"
|
||||
}
|
||||
}
|
||||
serverName := conn.ConnectionState().ServerName
|
||||
dh, ok := srv.DomainToHandler[strings.ToLower(serverName)]
|
||||
if !ok {
|
||||
log.Warn("gemini: failed to find domain handler", log.String("serverName", serverName))
|
||||
}
|
||||
srv.handle(dh, certificate, conn)
|
||||
}
|
||||
|
||||
// while this function could be inlined, exposing it makes it easier to test in isolation.
|
||||
func (srv *Server) handle(dh *DomainHandler, certificate Certificate, conn net.Conn) {
|
||||
start := time.Now()
|
||||
conn.SetReadDeadline(time.Now().Add(srv.ReadTimeout))
|
||||
r, ok, err := srv.parseRequest(conn)
|
||||
if err != nil {
|
||||
log.Info("gemini: failed to parse request", log.String("reason", err.Error()))
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r.Certificate = certificate
|
||||
ctx, cancel := context.WithTimeout(srv.Context, srv.HandlerTimeout)
|
||||
defer cancel()
|
||||
r.Context = ctx
|
||||
conn.SetWriteDeadline(time.Now().Add(srv.WriteTimeout))
|
||||
w := NewWriter(conn)
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
log.Error("gemini: server error", nil, log.String("url", r.URL.String()), log.Interface("recover", p))
|
||||
w.SetHeader(CodeCGIError, "internal error")
|
||||
}
|
||||
}()
|
||||
if certificate.Error != "" {
|
||||
w.SetHeader(CodeClientCertificateNotValid, certificate.Error)
|
||||
return
|
||||
}
|
||||
dh.Handler.ServeGemini(w, r)
|
||||
if w.Code == "" {
|
||||
log.Error("gemini: handler resulted in empty response", nil, log.String("url", r.URL.String()), log.String("handlerType", reflect.TypeOf(dh.Handler).PkgPath()))
|
||||
w.SetHeader(CodeCGIError, "empty response")
|
||||
}
|
||||
duration := time.Now().Sub(start)
|
||||
log.Info("gemini: response",
|
||||
log.String("url", r.URL.String()),
|
||||
log.String("path", r.URL.Path),
|
||||
log.String("code", w.Code),
|
||||
log.String("handlerType", reflect.TypeOf(dh.Handler).PkgPath()),
|
||||
log.Int64("ms", duration.Milliseconds()),
|
||||
log.Int64("us", int64(duration.Microseconds())),
|
||||
log.Int64("lenBody", w.WrittenBody),
|
||||
log.Int("lenHeader", w.WrittenHeader),
|
||||
log.Int64("len", int64(w.WrittenHeader)+w.WrittenBody),
|
||||
)
|
||||
}
|
||||
|
||||
func (srv *Server) parseRequest(rw io.ReadWriter) (r *Request, ok bool, err error) {
|
||||
request, ok, err := readUntilCrLf(rw, 1026)
|
||||
if err != nil && err != io.EOF {
|
||||
writeHeaderToWriter(CodeBadRequest, fmt.Sprintf("error reading request: %v", err), rw)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
log.Info("gemini: request too long or malformed", log.String("request", string(request)))
|
||||
writeHeaderToWriter(CodeBadRequest, "request too long or malformed", rw)
|
||||
return
|
||||
}
|
||||
ok = false
|
||||
url, err := url.Parse(strings.TrimSpace(string(request)))
|
||||
if err != nil {
|
||||
log.Info("gemini: malformed request", log.String("request", string(request)))
|
||||
writeHeaderToWriter(CodeBadRequest, "request malformed", rw)
|
||||
return
|
||||
}
|
||||
log.Info("gemini: received request", log.String("request", url.String()))
|
||||
r = &Request{
|
||||
URL: url,
|
||||
}
|
||||
return r, true, err
|
||||
}
|
||||
|
||||
// Writer passed to Gemini handlers.
|
||||
type Writer struct {
|
||||
Code string
|
||||
Writer io.Writer
|
||||
WrittenHeader int
|
||||
WrittenBody int64
|
||||
}
|
||||
|
||||
// NewWriter creates a new Gemini writer.
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{
|
||||
Writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrCannotWriteBodyWithoutSuccessCode = errors.New("gemini: cannot write body without success code")
|
||||
|
||||
func (gw *Writer) Write(p []byte) (n int, err error) {
|
||||
if gw.Code == "" {
|
||||
// Section 3.3
|
||||
gw.SetHeader(CodeSuccess, DefaultMIMEType)
|
||||
gw.Code = CodeSuccess
|
||||
}
|
||||
if !isSuccessCode(Code(gw.Code)) {
|
||||
err = ErrCannotWriteBodyWithoutSuccessCode
|
||||
return
|
||||
}
|
||||
n, err = gw.Writer.Write(p)
|
||||
gw.WrittenBody += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func isSuccessCode(code Code) bool {
|
||||
return len(code) == 2 && code[0] == '2'
|
||||
}
|
||||
|
||||
// ErrHeaderAlreadyWritten is returned by SetHeader when the Gemini header has already been written to the response.
|
||||
var ErrHeaderAlreadyWritten = errors.New("gemini: header already written")
|
||||
|
||||
func (gw *Writer) SetHeader(code Code, meta string) (err error) {
|
||||
if gw.Code != "" {
|
||||
return ErrHeaderAlreadyWritten
|
||||
}
|
||||
gw.Code = string(code)
|
||||
var n int
|
||||
n, err = writeHeaderToWriter(code, meta, gw.Writer)
|
||||
gw.WrittenHeader += n
|
||||
return
|
||||
}
|
||||
|
||||
func writeHeaderToWriter(code Code, meta string, w io.Writer) (n int, err error) {
|
||||
// <STATUS><SPACE><META><CR><LF>
|
||||
// Set default meta if required.
|
||||
if meta == "" && isSuccessCode(code) {
|
||||
meta = DefaultMIMEType
|
||||
}
|
||||
if len(meta) > 1024 {
|
||||
meta = meta[:1024]
|
||||
}
|
||||
return w.Write([]byte(string(code) + " " + meta + "\r\n"))
|
||||
}
|
||||
|
||||
// DomainHandler handles incoming requests for the ServerName using the provided KeyPair certificate
|
||||
// and Handler to process the request.
|
||||
type DomainHandler struct {
|
||||
ServerName string
|
||||
KeyPair tls.Certificate
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
// NewDomainHandler creates a new handler to listen for Gemini requests using TLS.
|
||||
// The cert can be generated by the github.com/a-h/gemini/cert.Generate package,
|
||||
// or can generated using openssl:
|
||||
// keyFile:
|
||||
// openssl ecparam -genkey -name secp384r1 -out server.key
|
||||
// certFile:
|
||||
// openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
func NewDomainHandler(serverName string, cert tls.Certificate, handler Handler) *DomainHandler {
|
||||
return &DomainHandler{
|
||||
ServerName: serverName,
|
||||
KeyPair: cert,
|
||||
Handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDomainHandlerFromFiles creates a new handler to listen for Gemini requests using TLS.
|
||||
// certFile / keyFile are links to the X509 keypair. This can be generated using openssl:
|
||||
// keyFile:
|
||||
// openssl ecparam -genkey -name secp384r1 -out server.key
|
||||
// certFile:
|
||||
// openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||
func NewDomainHandlerFromFiles(serverName, certFile, keyFile string, handler Handler) (*DomainHandler, error) {
|
||||
keyPair, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDomainHandler(serverName, keyPair, handler), nil
|
||||
}
|
||||
|
||||
// ListenAndServe starts up a new server to handle multiple domains with a specific certFile, keyFile and handler.
|
||||
func ListenAndServe(ctx context.Context, addr string, domains ...*DomainHandler) (err error) {
|
||||
if len(domains) == 0 {
|
||||
return fmt.Errorf("gemini: no default handler provided")
|
||||
}
|
||||
domainToHandler := make(map[string]*DomainHandler, len(domains))
|
||||
for i := 0; i < len(domains); i++ {
|
||||
domainToHandler[domains[i].ServerName] = domains[i]
|
||||
}
|
||||
server := NewServer(ctx, addr, domainToHandler)
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
Reference in New Issue
Block a user