-
Notifications
You must be signed in to change notification settings - Fork 12
/
site.go
336 lines (283 loc) · 8.33 KB
/
site.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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package main
import (
"errors"
"fmt"
"net/url"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/go-ini/ini"
)
type Site struct {
Domain string
CanonicalSecure bool
AuthUser string
AuthPass string
S3Host string
S3ForcePathStyle bool
BucketRegion string
BucketName string
UseImgix bool //deprecated
ResizingService string
ResizingServiceSecret string
ImageProxy string
BaseUrl string
AWS_SECRET_KEY_ID string `ini:"AWSKeyId"`
AWS_SECRET_KEY string `ini:"AWSKey"`
AWS_CLOUDFRONT_PRIVATE_KEY_PATH string `ini:"AWSCloudfrontKeyPath"`
AWS_CLOUDFRONT_PRIVATE_KEY_PAIR_ID string `ini:"AWSCloudfrontKeyPairId"`
CloudfrontPrivateKey *rsa.PrivateKey //this is loaded on config read
//from the path provided in AWS_PRIVATE_KEY_PATH
SiteTitle string
MetaTitle string
HasAlbumIndex bool
Albums []*Album
awsSession *session.Session
}
func GetPrivateKeyFromFile(path string) (*rsa.PrivateKey, error) {
// borrowed from: https://github.com/ianmcmahon/encoding_ssh
// read in private key from file (private key is PEM encoded PKCS)
bytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// decode PEM encoding to ANS.1 PKCS1 DER
block, _ := pem.Decode(bytes)
if block == nil {
return nil, errors.New("Private Key: No Block found in keyfile")
}
if block.Type != "RSA PRIVATE KEY" {
return nil, errors.New("Private Key: Unsupported key type, should be RSA Private key in pem file")
}
// parse DER format to a native type
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, errors.New("Private Key: could not parse RSA key")
}
return key, nil
}
func LoadSiteFromFile(path string) (*Site, error) {
cfg, err := ini.Load(path)
if err != nil {
return nil, err
}
defaultSection, err := cfg.GetSection("")
if err != nil {
return nil, err
}
s := &Site{}
if err := defaultSection.MapTo(s); err != nil {
return nil, err
}
if s.BucketRegion == "" && s.BucketName == "" {
s.BucketRegion = defaultSection.Key("Region").String()
s.BucketName = defaultSection.Key("Bucket").String()
}
for _, section := range cfg.Sections() {
if section.Name() == "DEFAULT" {
continue
}
if album, err := NewAlbumFromConfig(section, s); err != nil {
return nil, err
} else {
s.Albums = append(s.Albums, album)
}
}
if len(s.Albums) == 0 { // Check if the config is old style and create default album
if album, err := NewAlbum(s, "/", defaultSection.Key("Prefix").String(),
defaultSection.Key("AuthUser").String(), defaultSection.Key("AuthPass").String(),
defaultSection.Key("MetaTitle").String(), defaultSection.Key("AlbumTitle").String()); err != nil {
return nil, err
} else {
s.Albums = append(s.Albums, album)
}
}
if err := s.IsValid(); err != nil {
return nil, err
}
sess_config := &aws.Config{
Region: aws.String(s.BucketRegion),
Credentials: credentials.NewStaticCredentials(s.AWS_SECRET_KEY_ID, s.AWS_SECRET_KEY, ""),
}
if s.S3Host != "" {
sess_config.Endpoint = aws.String(s.S3Host)
}
if s.S3ForcePathStyle {
sess_config.S3ForcePathStyle = aws.Bool(true)
}
if sess, err := session.NewSession(sess_config); err != nil {
return nil, err
} else {
s.awsSession = sess
}
if err != nil {
return nil, err
}
if s.UseImgix {
//we've deprecated UseImgix as a config, but don't want
//to force users with valid configs to have their configs
//suddenly useless upon upgrade. We implicitly do this
//for the user, but if they try to set UseImgix
//AND a ResizingService, validation fails in IsValid.
s.ResizingService = "imgix"
}
// set up private key for thumbor+cloudfront, missing paths, etc
// are brought to our attention during validation
if s.ResizingService == "thumbor+cloudfront" {
s.CloudfrontPrivateKey, err = GetPrivateKeyFromFile(s.AWS_CLOUDFRONT_PRIVATE_KEY_PATH)
if err != nil {
return nil, err
}
}
return s, nil
}
func (s *Site) IsValid() error {
if s.Domain == "" || s.BucketRegion == "" || s.BucketName == "" || s.AWS_SECRET_KEY_ID == "" || s.AWS_SECRET_KEY == "" {
return errors.New("Domain, BucketRegion, BucketName, AWSKeyId, and AWSKey are required parameters that must have valid values")
}
if len(s.Albums) == 0 {
return errors.New("Can't have a site with 0 albums")
}
if s.HasAlbumIndex {
for _, a := range s.Albums {
if a.Path == "/" {
return errors.New("Site can't have an index and an album at path '/'")
}
}
}
if s.UseImgix && s.ResizingService != "" {
return errors.New("ResizingService supercedes UseImgix, please use ResizingService = imgix instead.")
}
switch s.ResizingService {
case "imgix", "":
break // All valid configs
case "thumbor":
if s.ResizingServiceSecret == "" {
return errors.New("Thumbor resizing service requires use of a shared secret for URL signing")
}
case "thumbor+cloudfront":
if s.AWS_CLOUDFRONT_PRIVATE_KEY_PATH == "" || s.AWS_CLOUDFRONT_PRIVATE_KEY_PAIR_ID == "" {
return errors.New("thumbor+cloudfront resizing service requires you to provision a private key " +
"and provide the path to the private key(config AWSCloudfrontKeyPath)," +
" along with the associated key pair id (config AWSCloudfrontKeyPairId)")
} else {
// we do the entire parse to ensure it's valid, however, do not assign
// to the config here as we don't want there to be surprises for developers
// (nobody expects a method called IsValid to be making assignments to config)
_, err := GetPrivateKeyFromFile(s.AWS_CLOUDFRONT_PRIVATE_KEY_PATH)
if err != nil {
return err
}
}
case "imageproxy":
if s.ImageProxy == "" {
return errors.New("ImageProxy requires proxy's URL")
}
default:
return fmt.Errorf("Unrecognized/Unimplemented resizing service '%s',"+
" valid options are imgix, thumbor, thumbor+cloudfront", s.ResizingService)
}
return nil
}
func (s *Site) HasAuth() bool {
return s.AuthUser != "" && s.AuthPass != ""
}
func (s *Site) GetAuthUser() string {
return s.AuthUser
}
func (s *Site) GetAuthPass() string {
return s.AuthPass
}
func (s *Site) GetCanonicalUrl() *url.URL {
proto, domain := "http", s.Domain
if s.CanonicalSecure {
proto = "https"
}
return &url.URL{
Scheme: proto,
Host: domain,
}
}
func (s *Site) GetAlbumsForIndex() []*Album {
indexAlbums := make([]*Album, 0)
for _, a := range s.Albums {
if a.InIndex {
indexAlbums = append(indexAlbums, a)
}
}
return indexAlbums
}
func (s *Site) GetS3Service() (*s3.S3, error) {
return s3.New(s.awsSession), nil
}
func (s *Site) GetPhotoForKey(key string) Renderable {
if s.ResizingService == "" {
return s.GetS3Photo(key)
} else {
return s.GetScaledPhoto(key)
}
}
func (s *Site) GetS3Photo(key string) *S3Photo {
return &S3Photo{
key,
s.BucketName,
s.awsSession,
}
}
func (s *Site) GetScaledPhoto(key string) Renderable {
if baseUrl, err := url.Parse(s.BaseUrl); err != nil {
fmt.Printf("Error trying to parse site base URL. Error: %s\n", err.Error())
return nil
} else {
if s.ResizingService == "imgix" {
return &ImgixRescaledPhoto{
RescaledPhoto: &RescaledPhoto{
key,
baseUrl,
},
}
} else if s.ResizingService == "thumbor" {
return &ThumborRaw{
RescaledPhoto: &RescaledPhoto{
key,
baseUrl,
},
Secret: s.ResizingServiceSecret,
}
} else if s.ResizingService == "thumbor+cloudfront" {
return &ThumborCloudfront{
RescaledPhoto: &RescaledPhoto{
key,
baseUrl,
},
AWSCloudfrontKeyPairId: s.AWS_CLOUDFRONT_PRIVATE_KEY_PAIR_ID,
AWSCloudfrontPrivateKey: s.CloudfrontPrivateKey,
}
} else if s.ResizingService == "imageproxy" {
return &ImageProxy{
S3Photo: s.GetS3Photo(key),
ImageProxy: s.ImageProxy,
}
} else {
// it should never come to this due to configuration validation,
// but best to keep the compiler happy.
return nil
}
}
}
func (s *Site) GetAlbumForPath(path string) (*Album, error) {
if path[len(path)-1] != '/' {
path = path + "/"
}
for _, album := range s.Albums {
if album.Path == path {
return album, nil
}
}
return nil, fmt.Errorf("Could not find album in site %s for path '%s'", s.Domain, path)
}