Index page and post pages ready
This commit is contained in:
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