forked from ThomasLeister/prosody-filer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprosody-filer.go
262 lines (227 loc) · 6.87 KB
/
prosody-filer.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
/*
* This module allows upload via mod_http_upload_external
* Also see: https://modules.prosody.im/mod_http_upload_external.html
*/
package main
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"flag"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/BurntSushi/toml"
minio "github.com/minio/minio-go"
"github.com/minio/minio-go/pkg/credentials"
)
/*
* Configuration of this server
*/
type Config struct {
Listenport string
Secret string
UploadSubDir string
ProxyMode bool
S3Endpoint string
S3AccessKey string
S3Secret string
S3TLS bool
S3Bucket string
}
var conf Config
var s3Client *minio.Client
const ALLOWED_METHODS string = "OPTIONS, HEAD, GET, PUT"
/*
* Sets CORS headers
*/
func addCORSheaders(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", ALLOWED_METHODS)
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "7200")
}
func addContentHeaders(h http.Header, filename string) {
ctype := mime.TypeByExtension(filepath.Ext(filename))
h.Set("Content-Type", ctype)
if m, _ := regexp.MatchString("((audio|image|video)/.*|text/plain)", ctype); m {
h.Set("Content-Disposition", "inline")
} else {
h.Set("Content-Disposition", "attachment")
}
}
/*
* Request handler
* Is activated when a clients requests the file, file information or an upload
*/
func handleRequest(w http.ResponseWriter, r *http.Request) {
log.Println("Incoming request:", r.Method, r.URL.String())
// Parse URL and args
u, err := url.Parse(r.URL.String())
if err != nil {
log.Println("Failed to parse URL:", err)
}
a, err := url.ParseQuery(u.RawQuery)
if err != nil {
log.Println("Failed to parse URL query params:", err)
}
fileStorePath := strings.TrimPrefix(u.Path, "/"+conf.UploadSubDir)
// Add CORS headers
addCORSheaders(w)
if r.Method == "PUT" {
// Check if MAC is attached to URL
if a["v"] == nil {
log.Println("Error: No HMAC attached to URL.")
http.Error(w, "Needs HMAC", 403)
return
}
/*
* Check if the request is valid
*/
mac := hmac.New(sha256.New, []byte(conf.Secret))
log.Println("fileStorePath:", fileStorePath)
log.Println("ContentLength:", strconv.FormatInt(r.ContentLength, 10))
mac.Write([]byte(fileStorePath + " " + strconv.FormatInt(r.ContentLength, 10)))
macString := hex.EncodeToString(mac.Sum(nil))
/*
* Check whether calculated (expected) MAC is the MAC that client send in "v" URL parameter
*/
if !hmac.Equal([]byte(macString), []byte(a["v"][0])) {
log.Println("Invalid MAC, expected:", macString)
http.Error(w, "403 Forbidden", 403)
return
}
ch := make(http.Header)
addContentHeaders(ch, fileStorePath)
// Somewhat redundant since we're setting these in the signed URL as well, but why not?
var opt minio.PutObjectOptions
opt.ContentType = ch.Get("Content-Type")
opt.ContentDisposition = ch.Get("Content-Disposition")
s3file, err := s3Client.PutObject(context.Background(), conf.S3Bucket, fileStorePath, r.Body, r.ContentLength, minio.PutObjectOptions{})
if err != nil {
log.Println("Uploading file failed:", err)
http.Error(w, "Backend Error", 502)
return
}
log.Println("Successfully stored file with ETag", s3file.ETag)
w.WriteHeader(http.StatusCreated)
} else if r.Method == "HEAD" || r.Method == "GET" {
if conf.ProxyMode {
obj, err := s3Client.GetObject(context.Background(), conf.S3Bucket, fileStorePath, minio.GetObjectOptions{})
if err != nil {
log.Println("Storage error:", err)
http.Error(w, "Storage error", 502)
return
}
addContentHeaders(w.Header(), fileStorePath)
// Content-Length for HEAD?
if r.Method == "GET" {
http.ServeContent(w, r, fileStorePath, time.Now(), obj)
}
} else {
ch := make(http.Header)
addContentHeaders(ch, fileStorePath)
uv := make(url.Values)
for k, v := range ch {
uv.Set("response-"+strings.ToLower(k), v[0])
}
// NOTE: This is an offline operation, using just our credentials, so it'll work for any URL,
// it's up to the S3 backend to 404 if the file isn't there.
url, err := s3Client.PresignedGetObject(context.Background(), conf.S3Bucket, fileStorePath, 24*time.Hour, uv)
if err != nil {
log.Println("Storage error:", err)
http.Error(w, "Storage error", 502)
return
}
w.Header().Set("Location", url.String())
w.WriteHeader(http.StatusFound) // better known as 302
}
} else if r.Method == "OPTIONS" {
w.Header().Set("Allow", ALLOWED_METHODS)
return
} else {
log.Println("Invalid method", r.Method)
http.Error(w, "405 Method Not Allowed", 405)
return
}
}
func readConfig(configfilename string, conf *Config) error {
log.Println("Reading configuration ...")
conf.S3TLS = true
configdata, err := ioutil.ReadFile(configfilename)
if err != nil {
log.Fatal("Configuration file config.toml cannot be read:", err, "...Exiting.")
return err
}
if _, err := toml.Decode(string(configdata), conf); err != nil {
log.Fatal("Config file config.toml is invalid:", err)
return err
}
// Support standard AWS credential env variables as well (will override whatever may have been in the config!)
if key, has := os.LookupEnv("AWS_ACCESS_KEY_ID"); has {
log.Println("Loading AWS credentials from evironment instead of config")
conf.S3AccessKey = key
}
if key, has := os.LookupEnv("AWS_SECRET_ACCESS_KEY"); has {
conf.S3Secret = key
}
return nil
}
func s3Login() {
var err error
s3Client, err = minio.New(conf.S3Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(conf.S3AccessKey, conf.S3Secret, ""),
Secure: conf.S3TLS,
})
if err != nil {
log.Fatalln(err)
}
exists, err := s3Client.BucketExists(context.Background(), conf.S3Bucket)
if err != nil {
log.Fatalln(err)
}
if !exists {
// Buggy example: Scaleway, appears to always report non-existent.
// But hey at least we've verified that the credentials work which is actually the main thing I want to check here.
log.Println("WARNING: Bucket does not exist (or S3 service is buggy): " + conf.S3Bucket)
}
}
/*
* Main function
*/
func main() {
/*
* Read startup arguments
*/
var argConfigFile = flag.String("config", "./config.toml", "Path to configuration file \"config.toml\".")
flag.Parse()
/*
* Read config file
*/
err := readConfig(*argConfigFile, &conf)
if err != nil {
log.Println("There was an error while reading the configuration file:", err)
}
log.Println("Starting Prosody-Filer-S3...")
s3Login()
log.Println("S3 bucket found.")
/*
* Start HTTP server
*/
http.HandleFunc("/"+conf.UploadSubDir, handleRequest)
log.Printf("Server started on %s. Waiting for requests.\n", conf.Listenport)
err = http.ListenAndServe(conf.Listenport, nil)
if err != nil {
log.Fatalln(err)
}
}