/
handler.go
267 lines (241 loc) · 7.4 KB
/
handler.go
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package main
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"log"
"mime"
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
func handleGeminiRequest(conn net.Conn, config Config, accessLogEntries chan LogEntry, errorLog *log.Logger) {
defer conn.Close()
var tlsConn (*tls.Conn) = conn.(*tls.Conn)
var log LogEntry
log.Time = time.Now()
log.RemoteAddr = conn.RemoteAddr()
log.RequestURL = "-"
log.Status = 0
defer func() { accessLogEntries <- log }()
// Read request
URL, err := readRequest(conn, &log, errorLog)
if err != nil {
return
}
// Enforce client certificate validity
clientCerts := tlsConn.ConnectionState().PeerCertificates
enforceCertificateValidity(clientCerts, conn, &log)
if log.Status != 0 {
return
}
// Reject non-gemini schemes
if URL.Scheme != "gemini" {
conn.Write([]byte("53 No proxying to non-Gemini content!\r\n"))
log.Status = 53
return
}
// Reject requests for content from other servers
if URL.Hostname() != config.Hostname || (URL.Port() != "" && URL.Port() != strconv.Itoa(config.Port)) {
conn.Write([]byte("53 No proxying to other hosts or ports!\r\n"))
log.Status = 53
return
}
// Fail if there are dots in the path
if strings.Contains(URL.Path, "..") {
conn.Write([]byte("50 Your directory traversal technique has been defeated!\r\n"))
log.Status = 50
return
}
// Resolve URI path to actual filesystem path
path := resolvePath(URL.Path, config)
// Paranoid security measures:
// Fail ASAP if the URL has mapped to a sensitive file
if path == config.CertPath || path == config.KeyPath || path == config.AccessLog || path == config.ErrorLog || filepath.Base(path) == ".molly" {
conn.Write([]byte("51 Not found!\r\n"))
log.Status = 51
return
}
// Read Molly files
if config.ReadMollyFiles {
parseMollyFiles(path, &config, errorLog)
}
// Check whether this URL is in a certificate zone
handleCertificateZones(URL, clientCerts, config, conn, &log)
if log.Status != 0 {
return
}
// Check for redirects
handleRedirects(URL, config, conn, &log, errorLog)
if log.Status != 0 {
return
}
// Check whether this URL is mapped to an SCGI app
for scgiPath, scgiSocket := range config.SCGIPaths {
if strings.HasPrefix(URL.Path, scgiPath) {
handleSCGI(URL, scgiPath, scgiSocket, config, &log, errorLog, conn)
return
}
}
// Check whether this URL is in a configured CGI path
for _, cgiPath := range config.CGIPaths {
if strings.HasPrefix(path, cgiPath) {
handleCGI(config, path, cgiPath, URL, &log, errorLog, conn)
if log.Status != 0 {
return
}
}
}
// Fail if file does not exist or perms aren't right
info, err := os.Stat(path)
if os.IsNotExist(err) || os.IsPermission(err) {
conn.Write([]byte("51 Not found!\r\n"))
log.Status = 51
return
} else if err != nil {
errorLog.Println("Error getting info for file " + path + ": " + err.Error())
conn.Write([]byte("40 Temporary failure!\r\n"))
log.Status = 40
return
} else if uint64(info.Mode().Perm())&0444 != 0444 {
conn.Write([]byte("51 Not found!\r\n"))
log.Status = 51
return
}
// Finally, serve the file or directory
if info.IsDir() {
serveDirectory(URL, path, &log, conn, config, errorLog)
} else {
serveFile(path, &log, conn, config, errorLog)
}
}
func readRequest(conn net.Conn, log *LogEntry, errorLog *log.Logger) (*url.URL, error) {
reader := bufio.NewReaderSize(conn, 1024)
request, overflow, err := reader.ReadLine()
if overflow {
conn.Write([]byte("59 Request too long!\r\n"))
log.Status = 59
return nil, errors.New("Request too long")
} else if err != nil {
errorLog.Println("Error reading request from " + conn.RemoteAddr().String() + ": " + err.Error())
conn.Write([]byte("40 Unknown error reading request!\r\n"))
log.Status = 40
return nil, errors.New("Error reading request")
}
// Parse request as URL
URL, err := url.Parse(string(request))
if err != nil {
errorLog.Println("Error parsing request URL " + string(request) + ": " + err.Error())
conn.Write([]byte("59 Error parsing URL!\r\n"))
log.Status = 59
return nil, errors.New("Bad URL in request")
}
log.RequestURL = URL.String()
// Set implicit scheme
if URL.Scheme == "" {
URL.Scheme = "gemini"
}
return URL, nil
}
func resolvePath(path string, config Config) string {
// Handle tildes
if strings.HasPrefix(path, "/~") {
bits := strings.Split(path, "/")
username := bits[1][1:]
new_prefix := filepath.Join(config.DocBase, config.HomeDocBase, username)
path = strings.Replace(path, bits[1], new_prefix, 1)
path = filepath.Clean(path)
} else {
path = filepath.Join(config.DocBase, path)
}
return path
}
func handleRedirects(URL *url.URL, config Config, conn net.Conn, log *LogEntry, errorLog *log.Logger) {
handleRedirectsInner(URL, config.TempRedirects, 30, conn, log, errorLog)
handleRedirectsInner(URL, config.PermRedirects, 31, conn, log, errorLog)
}
func handleRedirectsInner(URL *url.URL, redirects map[string]string, status int, conn net.Conn, log *LogEntry, errorLog *log.Logger) {
strStatus := strconv.Itoa(status)
for src, dst := range redirects {
compiled, err := regexp.Compile(src)
if err != nil {
errorLog.Println("Error compiling redirect regexp " + src + ": " + err.Error())
continue
}
if compiled.MatchString(URL.Path) {
URL.Path = compiled.ReplaceAllString(URL.Path, dst)
conn.Write([]byte(strStatus + " " + URL.String() + "\r\n"))
log.Status = status
return
}
}
}
func serveDirectory(URL *url.URL, path string, log *LogEntry, conn net.Conn, config Config, errorLog *log.Logger) {
// Redirect to add trailing slash if missing
// (otherwise relative links don't work properly)
if !strings.HasSuffix(URL.Path, "/") {
conn.Write([]byte(fmt.Sprintf("31 %s\r\n", URL.String()+"/")))
log.Status = 31
return
}
// Check for index.gmi if path is a directory
index_path := filepath.Join(path, "index."+config.GeminiExt)
index_info, err := os.Stat(index_path)
if err == nil && uint64(index_info.Mode().Perm())&0444 == 0444 {
serveFile(index_path, log, conn, config, errorLog)
// Serve a generated listing
} else {
listing, err := generateDirectoryListing(URL, path, config)
if err != nil {
errorLog.Println("Error generating listing for directory " + path + ": " + err.Error())
conn.Write([]byte("40 Server error!\r\n"))
log.Status = 40
return
}
conn.Write([]byte("20 text/gemini\r\n"))
log.Status = 20
conn.Write([]byte(listing))
}
}
func serveFile(path string, log *LogEntry, conn net.Conn, config Config, errorLog *log.Logger) {
// Get MIME type of files
ext := filepath.Ext(path)
var mimeType string
if ext == "."+config.GeminiExt {
mimeType = "text/gemini"
} else {
mimeType = mime.TypeByExtension(ext)
}
// Override extension-based MIME type
for pathRegex, newType := range config.MimeOverrides {
overridden, err := regexp.Match(pathRegex, []byte(path))
if err == nil && overridden {
mimeType = newType
}
}
// Set a generic MIME type if the extension wasn't recognised
if mimeType == "" {
mimeType = "application/octet-stream"
}
// Add lang parameter
if mimeType == "text/gemini" && config.DefaultLang != "" {
mimeType += "; lang=" + config.DefaultLang
}
contents, err := ioutil.ReadFile(path)
if err != nil {
errorLog.Println("Error reading file " + path + ": " + err.Error())
conn.Write([]byte("50 Error!\r\n"))
log.Status = 50
return
}
conn.Write([]byte(fmt.Sprintf("20 %s\r\n", mimeType)))
log.Status = 20
conn.Write(contents)
}