aboutsummaryrefslogtreecommitdiff
path: root/server.go
blob: 34d8070e950f9d0f503ae8e1fc29f26be38b56dd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package gemini

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"github.com/coreos/go-systemd/v22/activation"
	"io"
	"net"
	"strings"
)

// Request contains the data of the client request
type Request struct {
	URL string
}

// Handler is the interface a struct need to implement to be able to handle Gemini requests
type Handler interface {
	Handle(r Request) Response
}

// ListenAndServe create a TCP server on the specified address and pass
// new connections to the given handler.
// Each request is handled in a separate goroutine.
func ListenAndServe(addr, certFile, keyFile string, handler Handler) error {
	listeners, err := activation.Listeners()
	if err != nil {
		return err
	}
	if len(listeners) != 1 {
		return err
	}

	listener, err := listen(listeners[0], certFile, keyFile)
	if err != nil {
		return err
	}
	err = serve(listener, handler)
	if err != nil {
		return err
	}

	err = listener.Close()
	if err != nil {
		return fmt.Errorf("failed to close the listener: %v", err)
	}

	return nil
}

func listen(listener net.Listener, certFile, keyFile string) (net.Listener, error) {
	cer, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, fmt.Errorf("failed to load certificates: %v", err)
	}

	config := &tls.Config{Certificates: []tls.Certificate{cer}}
	ln := tls.NewListener(listener, config)

	return ln, nil
}

func serve(listener net.Listener, handler Handler) error {
	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}

		go handleConnection(conn, handler)
	}
}

func handleConnection(conn io.ReadWriteCloser, handler Handler) {
	defer conn.Close()

	requestURL, err := getRequestURL(conn)
	if err != nil {
		return
	}

	request := Request{requestURL}
	response := handler.Handle(request)

	if response.Body != nil {
		defer response.Body.Close()
	}

	err = writeResponse(conn, response)
	if err != nil {
		return
	}
}

func getRequestURL(conn io.Reader) (string, error) {
	scanner := bufio.NewScanner(conn)
	if ok := scanner.Scan(); !ok {
		return "", scanner.Err()
	}

	rawURL := scanner.Text()
	if strings.Contains(rawURL, "://") {
		return rawURL, nil
	}

	return fmt.Sprintf("gemini://%s", rawURL), nil
}

func writeResponse(conn io.Writer, response Response) error {
	_, err := fmt.Fprintf(conn, "%d %s\r\n", response.Status, response.Meta)
	if err != nil {
		return fmt.Errorf("failed to write header line to the client: %v", err)
	}

	if response.Body == nil {
		return nil
	}

	_, err = io.Copy(conn, response.Body)
	if err != nil {
		return fmt.Errorf("failed to write the response body to the client: %v", err)
	}

	return nil
}

// ErrorResponse create a response from the given error with the error string as the Meta field.
// If the error is of type gemini.Error, the status will be taken from the status field,
// otherwise it will default to StatusTemporaryFailure.
// If the error is nil, the function will panic.
func ErrorResponse(err error) Response {
	if err == nil {
		panic("nil error is not a valid parameter")
	}

	if ge, ok := err.(Error); ok {
		return Response{Status: ge.Status, Meta: ge.Error(), Body: nil}
	}

	return Response{Status: StatusTemporaryFailure, Meta: err.Error(), Body: nil}
}