-
Notifications
You must be signed in to change notification settings - Fork 13
/
cmd_blob_server.go
263 lines (229 loc) · 5.68 KB
/
cmd_blob_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
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
//
// Launch our blob-server.
//
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
"github.com/gorilla/mux"
)
// STORAGE holds a handle to our selected storage-method.
var STORAGE StorageHandler
// HealthHandler is a status end-point which can be polled remotely
// to test health.
func HealthHandler(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "alive")
}
// GetHandler allows a blob to be retrieved by name.
//
// This is called with requests like `GET /blob/XXXXXX`.
//
func GetHandler(res http.ResponseWriter, req *http.Request) {
var (
status int
err error
)
defer func() {
if nil != err {
http.Error(res, err.Error(), status)
}
}()
//
// Get the ID which is requested.
//
vars := mux.Vars(req)
id := vars["id"]
//
// We're in a chroot() so we shouldn't need to worry
// about relative paths. That said the chroot() call
// will have failed if we were not launched by root, so
// we need to make sure we avoid directory-traversal attacks.
//
r, _ := regexp.Compile("^([a-z0-9]+)$")
if !r.MatchString(id) {
status = http.StatusInternalServerError
err = errors.New("alphanumeric IDs only")
return
}
//
// If the request method was HEAD we don't need to
// lookup & return n the data, just see if it exists.
//
// We'll terminate early and just return the status-code
// 200 vs. 404.
//
if req.Method == "HEAD" {
res.Header().Set("Connection", "close")
if STORAGE.Exists(id) {
} else {
res.WriteHeader(http.StatusNotFound)
}
return
}
//
// If we reached this point then the request was a GET
// so we lookup the data, returning it if present.
//
data, meta := STORAGE.Get(id)
//
// The data was missing..
//
if data == nil {
http.NotFound(res, req)
} else {
//
// The meta-data will be used to populate the HTTP-response
// headers.
//
if meta != nil {
for k, v := range meta {
//
// Special case to set the content-type
// of the returned value.
//
if k == "X-Mime-Type" {
res.Header().Set(k, v)
k = "Content-Type"
}
//
// Add the response header.
//
res.Header().Set(k, v)
}
}
io.Copy(res, bytes.NewReader(*data))
}
}
// MissingHandler is a handler which is used as a fall-back if no matching
// handler is found.
func MissingHandler(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusNotFound)
fmt.Fprintf(res, "404 - content is not hosted here.")
}
// ListHandler returns the IDs of all blobs we know about.
//
// This is used by the replication utility.
func ListHandler(res http.ResponseWriter, req *http.Request) {
var list []string
list = STORAGE.Existing()
//
// If the list is non-empty then build up an array
// of the names, then send as JSON.
//
if len(list) > 0 {
mapB, _ := json.Marshal(list)
fmt.Fprintf(res, string(mapB))
} else {
fmt.Fprintf(res, "[]")
}
}
// UploadHandler is invoked to handle storing data in the blob-server.
func UploadHandler(res http.ResponseWriter, req *http.Request) {
var (
status int
err error
)
defer func() {
if nil != err {
http.Error(res, err.Error(), status)
}
}()
//
// Get the name of the blob to upload.
//
// We've previously chdir() and chroot() to the upload
// directory, so we don't need to worry about any path
// issues - providing the user isn't trying a traversal
// attack.
//
vars := mux.Vars(req)
id := vars["id"]
//
// Ensure the ID is entirely alphanumeric, to prevent
// traversal attacks.
//
r, _ := regexp.Compile("^([a-z0-9]+)$")
if !r.MatchString(id) {
err = errors.New("alphanumeric IDs only")
status = http.StatusInternalServerError
return
}
//
// Read the body of the request.
//
content, err := ioutil.ReadAll(req.Body)
if err != nil {
err = errors.New("failed to read body")
status = http.StatusInternalServerError
return
}
//
// If we received any X-headers in our request then save
// them to our extra-hash. These will be persisted and
// restored
//
extras := make(map[string]string)
for header, value := range req.Header {
if strings.HasPrefix(header, "X-") {
extras[header] = value[0]
}
}
//
// Store the body, via our interface.
//
result := STORAGE.Store(id, content, extras)
if result == false {
err = errors.New("failed to write to storage")
status = http.StatusInternalServerError
return
}
//
// Output the result - horrid.
//
// { "id": "foo",
// "size": 1234,
// "status": "ok",
// }
//
out := fmt.Sprintf("{\"id\":\"%s\",\"status\":\"OK\",\"size\":%d}", id, len(content))
fmt.Fprintf(res, string(out))
}
// blobServer is our entry-point to the sub-command.
func blobServer(options blobServerCmd) {
//
// Create a storage system.
//
// At the moment we only have a filesystem-based storage
// class. In the future it is possible we'd have more, and we'd
// choose between them via a command-line flag.
//
STORAGE = new(FilesystemStorage)
STORAGE.Setup(options.store)
//
// Create a new router and our route-mappings.
//
router := mux.NewRouter()
router.HandleFunc("/alive", HealthHandler).Methods("GET")
router.HandleFunc("/blob/{id}", GetHandler).Methods("GET")
router.HandleFunc("/blob/{id}", GetHandler).Methods("HEAD")
router.HandleFunc("/blob/{id}", UploadHandler).Methods("POST")
router.HandleFunc("/blobs", ListHandler).Methods("GET")
router.PathPrefix("/").HandlerFunc(MissingHandler)
http.Handle("/", router)
//
// Launch the server
//
fmt.Printf("blob-server available at http://%s:%d/\nUploads will be written beneath: %s\n",
options.host, options.port, options.store)
err := http.ListenAndServe(fmt.Sprintf("%s:%d", options.host, options.port), nil)
if err != nil {
panic(err)
}
}