aboutsummaryrefslogtreecommitdiff
path: root/server.go
diff options
context:
space:
mode:
authoradnano <[email protected]>2020-09-21 15:48:42 -0400
committeradnano <[email protected]>2020-09-21 15:49:09 -0400
commit257b8076756fa25e3b07d6d85e76874d1fe33268 (patch)
tree5cf003942047b7a4d25e8a16bdca3e856f600bd4 /server.go
downloadgo-gemini-257b8076756fa25e3b07d6d85e76874d1fe33268.tar.xz
go-gemini-257b8076756fa25e3b07d6d85e76874d1fe33268.zip
Initial commit
Diffstat (limited to 'server.go')
-rw-r--r--server.go185
1 files changed, 185 insertions, 0 deletions
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..24c6a15
--- /dev/null
+++ b/server.go
@@ -0,0 +1,185 @@
+package gemini
+
+import (
+ "bufio"
+ "crypto/tls"
+ "io"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// Status codes.
+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
+ StatusCertificateNotAuthorised = 61
+ StatusCertificateNotValid = 62
+)
+
+// Status code categories.
+const (
+ StatusClassInput = 1
+ StatusClassSuccess = 2
+ StatusClassRedirect = 3
+ StatusClassTemporaryFailure = 4
+ StatusClassPermanentFailure = 5
+ StatusClassClientCertificateRequired = 6
+)
+
+// Response is a Gemini response.
+type Response struct {
+ Status int
+ Meta string
+ Body []byte
+}
+
+// Write writes the Response to the provided io.Writer.
+func (r *Response) Write(w io.Writer) {
+ header := strconv.Itoa(r.Status) + " " + r.Meta + "\r\n"
+ w.Write([]byte(header))
+
+ // Only write response body on success
+ if r.Status/10 == StatusClassSuccess {
+ w.Write(r.Body)
+ }
+}
+
+// Server is a Gemini server.
+type Server struct {
+ Addr string
+ TLSConfig *tls.Config
+ Handler Handler
+}
+
+// ListenAndServer listens on the given address and serves.
+func (s *Server) ListenAndServe() error {
+ addr := s.Addr
+ if addr == "" {
+ addr = ":1965"
+ }
+
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
+ }
+ defer ln.Close()
+
+ tlsListener := tls.NewListener(ln, s.TLSConfig)
+ return s.Serve(tlsListener)
+}
+
+// Serve listens for requests on the provided listener.
+func (s *Server) Serve(ln net.Listener) error {
+ for {
+ rw, err := ln.Accept()
+ if err != nil {
+ return err
+ }
+
+ req, err := readLine(rw)
+ if err != nil {
+ continue
+ }
+ url, err := url.Parse(req)
+ if err != nil {
+ continue
+ }
+ resp := s.Handler.Serve(url)
+ resp.Write(rw)
+ rw.Close()
+ }
+}
+
+// Handler handles a url with a response.
+type Handler interface {
+ // Serve accepts a url, as that is the only information that the client
+ // provides in a request.
+ Serve(*url.URL) *Response
+}
+
+// Mux is a Gemini request multiplexer.
+// It matches the URL of each incoming request against a list of registered
+// patterns and calls the handler for the pattern that most closesly matches
+// the URL.
+type Mux struct {
+ entries []muxEntry
+}
+
+type muxEntry struct {
+ scheme string
+ host string
+ path string
+ handler Handler
+}
+
+func (m *Mux) match(url *url.URL) Handler {
+ for _, e := range m.entries {
+ if (e.scheme == "" || url.Scheme == e.scheme) &&
+ (e.host == "" || url.Host == e.host) &&
+ strings.HasPrefix(url.Path, e.path) {
+ return e.handler
+ }
+ }
+ return nil
+}
+
+func (m *Mux) Handle(pattern string, handler Handler) {
+ url, err := url.Parse(pattern)
+ if err != nil {
+ panic(err)
+ }
+ m.entries = append(m.entries, muxEntry{
+ url.Scheme,
+ url.Host,
+ url.Path,
+ handler,
+ })
+}
+
+func (m *Mux) HandleFunc(pattern string, handlerFunc func(url *url.URL) *Response) {
+ handler := HandlerFunc(handlerFunc)
+ m.Handle(pattern, handler)
+}
+
+func (m *Mux) Serve(url *url.URL) *Response {
+ h := m.match(url)
+ if h == nil {
+ return &Response{
+ Status: StatusNotFound,
+ Meta: "Not found",
+ }
+ }
+ return h.Serve(url)
+}
+
+type HandlerFunc func(url *url.URL) *Response
+
+func (f HandlerFunc) Serve(url *url.URL) *Response {
+ return f(url)
+}
+
+// readLine reads a line.
+func readLine(r io.Reader) (string, error) {
+ scanner := bufio.NewScanner(r)
+ scanner.Scan()
+ if err := scanner.Err(); err != nil {
+ return "", err
+ }
+ return scanner.Text(), nil
+}