-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathpaths.go
303 lines (248 loc) · 7.17 KB
/
paths.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
package path
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/t94j0/satellite/net/http"
"github.com/t94j0/satellite/satellite/geoip"
)
// Paths is the compilation of parsed paths
type Paths struct {
base string
pathsList string
dbRoot string
globalConditionsPath string
state *State
GeoipDB geoip.DB
list []*Path
}
// New creates a new Paths variable from the specified base path
func New(serverRoot, pathsList, dbPath, gcp string) (*Paths, error) {
list := make([]*Path, 0)
state, err := NewState(path.Join(serverRoot, dbPath))
if err != nil {
return nil, err
}
ret := &Paths{
base: serverRoot,
pathsList: path.Join(serverRoot, pathsList),
dbRoot: dbPath,
globalConditionsPath: gcp,
list: list,
state: state,
}
if err := ret.Reload(); err != nil {
return ret, err
}
return ret, nil
}
// NewDefault instantiates a Paths object with default configuration
func NewDefault(serverRoot, gcp string) (*Paths, error) {
return New(serverRoot, "pathList.yml", ".db", gcp)
}
// NewDefaultTest For many of the tests, we don't need to apply the global conditionals, so this helper function is for test cases
func NewDefaultTest(serverRoot string) (*Paths, error) {
return New(serverRoot, "pathList.yml", ".db", "")
}
// AddGeoIP adds the GeoIP path to this location
func (paths *Paths) AddGeoIP(path string) error {
db, err := geoip.New(path)
if err != nil {
return err
}
paths.GeoipDB = db
return nil
}
// Len gets the number of paths
func (paths *Paths) Len() int {
return len(paths.list)
}
// Match matches a page given a URI. It returns the specified Path and a boolean
// value to determine if there was a page that matched the URI
func (paths *Paths) Match(uri string) (*Path, bool) {
hostedFileFromPath := func(v *Path) (*Path, bool) {
if v.HostedFile != "" {
return v, true
}
if _, err := os.Stat(path.Join(paths.base, v.Path)); err == nil {
v.HostedFile = v.Path
} else {
v.HostedFile = uri
}
return v, true
}
// Prioritize direct matches over globs
for _, v := range paths.list {
if v.Path == uri {
return hostedFileFromPath(v)
}
}
// Secondarily accept globs. Path is indeterminate if multiple globs match
for _, v := range paths.list {
g := glob.MustCompile(v.Path, '/')
if g.Match(uri) {
return hostedFileFromPath(v)
}
}
info, err := os.Stat(path.Join(paths.base, uri))
if err == nil && !info.IsDir() {
return &Path{Path: uri, HostedFile: uri}, true
}
return nil, false
}
// ingestPathList adds the proxy from target path if it exists
func (paths *Paths) ingestPathList() ([]*Path, error) {
pathsList := paths.pathsList
if _, err := os.Stat(pathsList); os.IsNotExist(err) {
// Do not fail if the pathList does not exist
return []*Path{}, nil
}
pathArr, err := NewPathArray(pathsList)
if err != nil {
return []*Path{}, err
}
return pathArr, nil
}
func (paths *Paths) collectConditionalsDirectory(targetPath string) (RequestConditions, error) {
condsResult := make([]RequestConditions, 0)
collectWalkFunc := func(oPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
condData, err := ioutil.ReadFile(oPath)
if err != nil {
return err
}
conds, err := NewRequestConditions(condData)
if err != nil {
return err
}
condsResult = append(condsResult, conds)
return nil
}
if err := filepath.Walk(targetPath, collectWalkFunc); err != nil {
return RequestConditions{}, err
}
mergedConds, err := MergeRequestConditions(condsResult...)
if err != nil {
return RequestConditions{}, err
}
return mergedConds, nil
}
func (paths *Paths) validate(pathList []*Path) error {
for _, v := range pathList {
// Ensure all path URI globbing compiles
if _, err := glob.Compile(v.Path); err != nil {
return errors.Wrap(err, "unable to compile glob: "+v.Path)
}
// Ensure paths are backed up by a file
// fmt.Println(v.Path)
}
return nil
}
// Reload refreshes the list of paths internally to Paths
func (paths *Paths) Reload() error {
pathsList, err := paths.ingestPathList()
if err != nil {
return err
}
if err := paths.validate(pathsList); err != nil {
return err
}
paths.list = pathsList
return nil
}
// Serve serves a page without checking conditionals
func (paths *Paths) Serve(w http.ResponseWriter, req *http.Request) error {
uri := req.URL.Path
targetPath, exists := paths.Match(uri)
if !exists {
return errors.New("not_found render page not found")
}
if err := targetPath.ServeHTTP(w, req, paths.base); err != nil {
return err
}
return nil
}
func getAllConditionals(uri string, paths *Paths, matchedPath *Path) (RequestConditions, error) {
target := matchedPath.Conditions
globalConditions, err := paths.getGlobalConditionals()
if err != nil {
return target, err
}
matchingConditions, err := paths.getMatchingConditionals(uri)
if err != nil {
return RequestConditions{}, err
}
return MergeRequestConditions(globalConditions, matchingConditions, target)
}
// getMatchingConditionals gets all conditions that apply to `uri` (since some paths can be globbed) and apply them to matchedPath.Conditions
func (paths *Paths) getMatchingConditionals(uri string) (RequestConditions, error) {
conditions := make([]RequestConditions, 0)
for _, path := range paths.list {
g := glob.MustCompile(path.Path, '/')
if g.Match(uri) {
conditions = append(conditions, path.Conditions)
}
}
return MergeRequestConditions(conditions...)
}
// getGlobalConditionals gets all conditions from the paths.globalConditionsPath
func (paths *Paths) getGlobalConditionals() (RequestConditions, error) {
var empty RequestConditions
gcp := paths.globalConditionsPath
if gcp == "" {
return empty, nil
}
if f, err := os.Stat(gcp); err != nil || !f.IsDir() {
return empty, nil
}
globalConditions, err := paths.collectConditionalsDirectory(gcp)
if err != nil {
return empty, err
}
return globalConditions, nil
}
// MatchAndServe matches a path, determines if the path should be served, and serves the file based on an HTTP request. If a failure occurs, this function will serve failed pages.
//
// This is a helper function which combines already-exposed functions to make file serving easy.
//
// Returns true when the file was served and false when a 404 page should be returned
func (paths *Paths) MatchAndServe(w http.ResponseWriter, req *http.Request) (bool, error) {
uri := req.URL.Path
matchedPath, exists := paths.Match(uri)
if !exists {
return false, nil
}
conditions, err := getAllConditionals(uri, paths, matchedPath)
if err != nil {
return false, err
}
if conditions.ShouldHost(req, paths.state, paths.GeoipDB) {
paths.state.Hit(req)
if err := matchedPath.ServeHTTP(w, req, paths.base); err != nil {
return false, err
}
return true, nil
}
if matchedPath.FailRedirect(w, req) {
return true, nil
}
matched, err := matchedPath.FailRender(w, req, func(uri string) *Path {
newPath, found := paths.Match(matchedPath.OnFailure.Render)
if !found {
return nil
}
return newPath
}, paths.base)
if err != nil {
return false, err
}
return matched, nil
}