Index page and post pages ready

This commit is contained in:
Senad Uka
2023-07-30 19:21:16 +02:00
parent 327e9caad7
commit b1561c4875
88 changed files with 273478 additions and 2 deletions

4
vendor/github.com/a-h/gemini/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}