-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.go
187 lines (162 loc) · 5.46 KB
/
server.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
package main
import (
"bytes"
"errors"
"io"
"net/http"
"os"
"strings"
"text/template"
log "github.com/sirupsen/logrus"
)
// Viewmodel encapsulates a means of accessing all template data associated with a view
type Viewmodel interface {
// Data accepts the current request context, and the current directory being indexed
Data() interface{}
// Funcs gets a funcmap to expose to views
Funcs() template.FuncMap
}
// ViewmodelBuilder builds a Viewmodel for each index request, passing request and directory state
type ViewmodelBuilder func(*http.Request, http.File) (Viewmodel, error)
const defaultIndexFile = "index.gohtml"
type indexHandler struct {
index string
root http.FileSystem
newViewmodel ViewmodelBuilder
}
/*
ServeHTTP serves directories that have index files, or file contents if the request doesn't target a directory.
Borrows heavily from net/http.fileHandler source code.
*/
func (i *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var c io.ReadSeeker // the content that will ultimately be served by http.ServeContent for a successful request. Not necessarily a file.
path := r.URL.Path // the sanitised path to use to target files
if !strings.HasPrefix(path, "/") {
path = "/" + path
r.URL.Path = path
}
// direct requests to a dir's index file should be redirected to the parent dir's path (or should they?)
// if strings.HasSuffix(path, i.index) {
// TODO: redirct this
// return
// {
// can we open a file using the request path?
f, err := i.root.Open(path)
if err != nil {
_, code := toHTTPError(err)
log.Error(err)
http.Error(w, http.StatusText(code), code)
return
}
defer f.Close()
c = f // for now, let's assume this is the source of content to serve
// file stat modtime is used for Last-Modified headers, etc.
s, err := f.Stat()
if err != nil {
_, code := toHTTPError(err)
log.Error(err)
http.Error(w, http.StatusText(code), code)
return
}
// when the req targets a dir, we search for an index file inside
if s.IsDir() {
if path[len(path)-1] != '/' {
// TODO redirect to canonical
path = path + "/"
}
log.Debugf("Request '%s' targets a directory - searching dir for index '%s'", path, i.index)
fi, err := i.root.Open(path + i.index)
if err != nil {
// if the dir is missing an index, we 404 (why would we assume an auto-indexed page is desired??)
_, code := toHTTPError(err)
log.Error(err)
http.Error(w, http.StatusText(code), code)
return
}
defer fi.Close()
si, err := fi.Stat()
if err != nil {
_, code := toHTTPError(err)
log.Error(err)
http.Error(w, http.StatusText(code), code)
return
}
// the index file is our template
bi := make([]byte, si.Size())
bytesRead, err := fi.Read(bi)
if err != nil {
_, code := toHTTPError(err)
log.Errorf("Error reading template file '%s': %s", si.Name(), err.Error())
http.Error(w, http.StatusText(code), code)
return
}
log.Debugf("Reading %d bytes from index template '%s%s'", bytesRead, path, i.index)
// construct a viewmodel for the request/view
vm, err := i.newViewmodel(r, f)
if err != nil {
_, code := toHTTPError(err)
log.Error(err)
http.Error(w, http.StatusText(code), code)
return
}
t, err := template.New("bodyTemplate").
Funcs(vm.Funcs()).
Parse(string(bi))
if err != nil {
_, code := toHTTPError(err)
log.Errorf("Error parsing template: %s", err.Error())
http.Error(w, http.StatusText(code), code)
return
}
// render page into a buffer
rb := new(bytes.Buffer)
err = t.Execute(rb, vm.Data())
if err != nil {
_, code := toHTTPError(err)
log.Errorf("Error executing response template with model: %s", err.Error())
http.Error(w, http.StatusText(code), code)
return
}
log.Debugf("Rendered %d bytes from '%s%s'", len(rb.Bytes()), path, i.index)
c = bytes.NewReader(rb.Bytes())
}
log.Debugf("Serving content for request to '%s'", path)
http.ServeContent(w, r, s.Name(), s.ModTime(), c)
_ = f.Close()
}
/*
IndexServer acts much like the native http.FileServer implementation.
However, IndexServer will *not* autoindex; if a directory has no index file, requests to it will 404.
Requests to non-directory files will serve the file's contents.
The name of directory index files is customisable via the 'index' argument - pass "" for the default designated by defaultIndexFile.
*/
func IndexServer(root http.FileSystem, index string, v ViewmodelBuilder) (http.Handler, error) {
if index == "" {
index = defaultIndexFile
}
if strings.Contains(index, "/") {
return nil, errors.New("invalid index filename passed: name should not include a leading slash, or parent directories")
}
return &indexHandler{
root: root,
index: index,
newViewmodel: v,
}, nil
}
// Copyright (c) 2009 The Go Authors. All rights reserved.
// -------------------------------------------------------
// toHTTPError returns a non-specific HTTP error message and status code
// for a given non-nil error value. It's important that toHTTPError does not
// actually return err.Error(), since msg and httpStatus are returned to users,
// and historically Go's ServeContent always returned just "404 Not Found" for
// all errors. We don't want to start leaking information in error messages.
func toHTTPError(err error) (msg string, httpStatus int) {
if os.IsNotExist(err) {
return "404 page not found", http.StatusNotFound
}
if os.IsPermission(err) {
return "403 Forbidden", http.StatusForbidden
}
// Default:
return "500 Internal Server Error", http.StatusInternalServerError
}