-
Notifications
You must be signed in to change notification settings - Fork 0
/
handler.go
166 lines (141 loc) · 4.8 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
package akhttpd
import (
"context"
_ "embed" // include default robots.txt and index.html
"io"
"log"
"net/http"
"os"
"regexp"
"strings"
"github.com/tkw1536/akhttpd/pkg/format"
"github.com/tkw1536/akhttpd/pkg/repo"
)
// spellchecker:words akhttpd
// Handler is the main akhttpd Server Handler.
// It implements http.Handler, see the ServerHTTP method.
type Handler struct {
repo.KeyRepository
Formatters map[string]format.Formatter
SuffixHTMLPath string // if non-empty, path to append to every html response
IndexHTMLPath string // if non-empty, path to serve index.html from
RobotsTXTPath string // if non-empty, path to serve robots.txt from
}
// RegisterFormatter registers formatter as the formatter for the provided extension.
// When extension is empty, registers it for the path without an extension.
func (h *Handler) RegisterFormatter(extension string, formatter format.Formatter) {
if h.Formatters == nil {
h.Formatters = make(map[string]format.Formatter)
}
h.Formatters[strings.ToLower(extension)] = formatter
}
// WriteSuffix writes the html suffix to w
func (h Handler) WriteSuffix(w io.Writer) error {
if h.SuffixHTMLPath == "" {
return nil
}
// open the file for reading
f, err := os.Open(h.SuffixHTMLPath)
if err != nil {
return err
}
defer f.Close()
// write the suffix into the writer
_, err = io.Copy(w, f)
return err
}
var handlerPath = regexp.MustCompile(`^/[a-zA-Z\d-@]+((\.[a-zA-Z]+)|/[a-zA-Z_]+)?/?$`)
//go:embed resources/index.min.html
var defaultIndexHTML []byte
//go:embed resources/robots.txt
var defaultRobotsTXT []byte
// ServerHTTP serves the main akhttpd server.
// It only answers to GET requests, all other requests are answered with Method Not Allowed.
// Whenever something goes wrong, responds with "Internal Server Error" and logs the error.
//
// This method only responds successfully to a few URLS.
// All other URLs result in a HTTP 404 Response.
//
// GET /
// GET /index.html
//
// When IndexHTMLPath is not the empty string, sends back the file with Status HTTP 200.
// When IndexHTMLPath is empty, it sends back a default index.html file.
//
// GET /${username}
// GET /${username}.${formatter}, GET /${username}/${formatter}
//
// Fetches SSH Keys for the provided user and formats them with formatter.
// When formatter is omitted, uses the default formatter.
// If the formatter or user do not exist, returns HTTP 404.
//
// GET /robots.txt
//
// When RobotsTXTPath is not the empty string, sends back the file with Status HTTP 200.
// When RobotsTXTPath is empty, it sends back a default robots.txt file.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ensure that only a GET is used, we don't support anything else
// this includes just requesting HEAD.
if r.Method != http.MethodGet {
w.Header().Add("Allow", "GET")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
path := r.URL.Path
switch {
case r.Method != http.MethodGet:
case path == "/", path == "":
err := handlePathOrFallback(w, h.IndexHTMLPath, defaultIndexHTML, "text/html")
if err != nil {
return
}
h.WriteSuffix(w)
case path == "/robots.txt":
handlePathOrFallback(w, h.RobotsTXTPath, defaultRobotsTXT, "text/plain")
case path == "/favicon.ico": // performance optimization as web browsers frequently request this
http.NotFound(w, r)
case handlerPath.MatchString(path): // the main route, where the bulk of handling takes place
path = strings.Trim(path, "/")
var ext string
// handle both '.' and '/' as an index
idx := strings.IndexRune(path, '.')
if idx == -1 {
idx = strings.IndexRune(path, '/')
}
if idx != -1 {
ext = path[idx+1:]
path = path[:idx]
}
h.serveAuthorizedKey(w, r, path, ext)
default: // everything else isn't found
http.NotFound(w, r)
}
}
// serveAuthorizedKey serves an authorized_keys file for a given user
func (h Handler) serveAuthorizedKey(w http.ResponseWriter, r *http.Request, username, formatName string) {
formatter, hasFormatter := h.Formatters[strings.ToLower(formatName)]
if !hasFormatter {
http.NotFound(w, r)
return
}
source, keys, err := h.KeyRepository.GetKeys(context.Background(), username)
if err != nil {
if _, isNotFound := err.(repo.UserNotFoundError); isNotFound {
http.NotFound(w, r)
return
}
if _, isLegalUnavailable := err.(repo.UserNotAvailableError); isLegalUnavailable {
http.Error(w, "Unavailable for legal reasons", http.StatusUnavailableForLegalReasons)
return
}
log.Printf("%s: Internal Server Error: %s", r.URL.Path, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
n, err := formatter.WriteTo(username, source, keys, r, w)
if n == 0 && err != nil {
log.Printf("%s: Internal Server Error: %s", r.URL.Path, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}