gmifs/gemini/gemini.go

239 lines
5.6 KiB
Go
Raw Normal View History

2021-07-07 19:39:16 +03:00
package gemini
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"net/url"
"path"
"strings"
"time"
"unicode/utf8"
)
const (
StatusInput = 10
StatusSensitiveInput = 11
StatusSuccess = 20
StatusRedirectTemporary = 30
StatusRedirectPermanent = 31
StatusTemporaryFailure = 40
StatusServerUnavailable = 41
StatusCgiError = 42
StatusProxyError = 43
StatusSlowDown = 44
StatusPermanentFailure = 50
StatusNotFound = 51
StatusGone = 52
StatusProxyRequestRefused = 53
StatusBadRequest = 59
StatusClientCertificateRequired = 60
StatusCertificateNotAuthorized = 61
StatusCertificateNotValid = 62
)
const (
Termination = "\r\n"
URLMaxBytes = 1024
IndexFile = "index.gmi"
MimeType = "text/gemini; charset=utf-8"
)
var (
ErrServerClosed = errors.New("gemini: server closed")
ErrHeaderTooLong = errors.New("gemini: header too long")
ErrMissingFile = errors.New("gemini: no such file")
)
type Request struct {
2021-07-08 04:34:32 +03:00
ctx context.Context
URL *url.URL
RemoteAddr string
// RequestURI is the unmodified request-target of the Request-Line as sent by the client
// to a server. Usually the URL field should be used instead.
RequestURI string
2021-07-07 19:39:16 +03:00
}
type Handler interface {
ServeGemini(io.Writer, *Request)
}
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as Gemini handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(io.Writer, *Request)
// ServeGemini calls f(w, r).
func (f HandlerFunc) ServeGemini(w io.Writer, r *Request) {
f(w, r)
}
type Server struct {
// Addr is the address the server is listening on.
Addr string
// Hostname or common name of the server. This is used for absolute redirects.
Hostname string
TLSConfig *tls.Config
Handler Handler // handler to invoke
ReadTimeout time.Duration
MaxOpenConns int
}
func (s *Server) ListenAndServe() error {
// outer for loop, if listener closes we will restart it. This may be useful if we switch out
// TLSConfig.
//for {
listener, err := tls.Listen("tcp", s.Addr, s.TLSConfig)
if err != nil {
return fmt.Errorf("gemini server listen: %w", err)
}
queue := make(chan net.Conn, s.MaxOpenConns)
go s.handleConnectionQueue(queue)
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("server: accept: %s", err)
break
}
queue <- conn
}
//}
return nil
}
func (s *Server) handleConnectionQueue(queue chan net.Conn) {
// semaphore for connection limiter
type semaphore chan struct{}
sem := make(semaphore, s.MaxOpenConns)
for {
// for each connection we receive
conn := <-queue
sem <- struct{}{} // acquire
go s.handleConnection(conn, sem)
}
}
func (s *Server) handleConnection(conn net.Conn, sem chan struct{}) {
defer func() {
conn.Close()
<-sem // release
}()
reqChan := make(chan request)
// push job for which we allocated a sem slot and wait
go requestChannel(conn, reqChan)
select {
case header := <-reqChan:
if header.err != nil {
s.handleRequestError(conn, header)
return
}
ctx := context.Background()
2021-07-08 04:34:32 +03:00
r := &Request{
ctx: ctx,
URL: header.URL,
RequestURI: header.rawuri,
RemoteAddr: conn.RemoteAddr().String(),
}
2021-07-07 19:39:16 +03:00
s.Handler.ServeGemini(conn, r)
case <-time.After(s.ReadTimeout):
WriteHeader(conn, StatusServerUnavailable, "")
}
}
func (s *Server) handleRequestError(conn net.Conn, req request) {
// TODO: log err
var gmierr *GmiError
if errors.As(req.err, &gmierr) {
WriteHeader(conn, gmierr.Code, gmierr.Error())
return
}
// this path doesn't exist currently.
WriteHeader(conn, StatusTemporaryFailure, "internal")
}
// conn handler
type request struct {
2021-07-08 04:34:32 +03:00
rawuri string
URL *url.URL
err error
2021-07-07 19:39:16 +03:00
}
func requestChannel(c net.Conn, rsp chan request) {
2021-07-08 04:34:32 +03:00
r, err := readHeader(c)
r.err = err
rsp <- *r
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
func readHeader(c net.Conn) (*request, error) {
r := &request{}
2021-07-07 19:39:16 +03:00
req, err := bufio.NewReader(c).ReadString('\r')
if err != nil {
return nil, Error(StatusTemporaryFailure, errors.New("error reading request"))
}
2021-07-08 04:34:32 +03:00
r.rawuri = req
2021-07-07 19:39:16 +03:00
requestURL := strings.TrimSpace(req)
if requestURL == "" {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, errors.New("empty request URL"))
2021-07-07 19:39:16 +03:00
} else if !utf8.ValidString(requestURL) {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, errors.New("not a valid utf-8 url"))
2021-07-07 19:39:16 +03:00
} else if len(requestURL) > URLMaxBytes {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, ErrHeaderTooLong)
2021-07-07 19:39:16 +03:00
}
parsedURL, err := url.Parse(requestURL)
if err != nil {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, err)
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
r.URL = parsedURL
2021-07-07 19:39:16 +03:00
if parsedURL.Scheme != "" && parsedURL.Scheme != "gemini" {
2021-07-08 04:34:32 +03:00
return r, Error(StatusProxyRequestRefused, fmt.Errorf("unknown protocol scheme %s", parsedURL.Scheme))
2021-07-07 19:39:16 +03:00
} else if parsedURL.Host == "" {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, errors.New("empty host"))
2021-07-07 19:39:16 +03:00
}
if parsedURL.Path == "" {
2021-07-08 04:34:32 +03:00
return r, Error(StatusRedirectPermanent, errors.New("./"+parsedURL.Path))
2021-07-07 19:39:16 +03:00
} else if parsedURL.Path != path.Clean(parsedURL.Path) {
2021-07-08 04:34:32 +03:00
return r, Error(StatusBadRequest, errors.New("path error"))
2021-07-07 19:39:16 +03:00
}
2021-07-08 04:34:32 +03:00
return r, nil
2021-07-07 19:39:16 +03:00
}
func (s *Server) Shutdown(ctx context.Context) error {
return nil
}
func WriteHeader(c io.Writer, code int, message string) {
// <STATUS><SPACE><META><CR><LF>
var header []byte
if len(message) == 0 {
header = []byte(fmt.Sprintf("%d%s", code, Termination))
}
header = []byte(fmt.Sprintf("%d %s%s", code, message, Termination))
c.Write(header)
}
func Write(c io.Writer, body []byte) {
reader := bytes.NewReader(body)
io.Copy(c, reader)
}