This repository has been archived by the owner on Jul 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
config.go
329 lines (307 loc) · 8.89 KB
/
config.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
package inkfish
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
const PasswordHashLen = 64 // Length of SHA-256 output, as hex chars
type Acl struct {
From []string
Entries []AclEntry
MitmBypass []*regexp.Regexp
}
type AclEntry struct {
AllMethods bool
Methods []string
Pattern *regexp.Regexp
Quiet bool
}
type UserEntry struct {
Username string
PasswordHash string
}
func listContainsString(haystack []string, needle string) bool {
// Return true iff needle is present in haystack
for _, s := range haystack {
if s == needle {
return true
}
}
return false
}
func (proxy *Inkfish) findAclEntryThatAllowsRequest(from, method, url string) *AclEntry {
// Check each acl in the config to see if it permits the request
for _, aclConfig := range proxy.Acls {
if aclEntry := aclConfig.findAclEntryThatAllowsRequest(from, method, url); aclEntry != nil {
return aclEntry
}
}
return nil
}
func (proxy *Inkfish) bypassMitm(from, hostAndPort string) bool {
for _, aclConfig := range proxy.Acls {
if aclConfig.bypassMitm(from, hostAndPort) {
return true
}
}
return false
}
func (proxy *Inkfish) credentialsAreValid(user, password string) bool {
// Check each UserEntry to see if it matches the provided credentials
hashedPw := sha256.Sum256([]byte(password))
for _, ent := range proxy.Passwd {
if ent.Username == user {
// It's possible to have multiple passwords for the same user,
// this allows blue/green credentials. Otherwise we could early-exit.
actualPw, err := hex.DecodeString(ent.PasswordHash)
if err != nil {
return false
}
if subtle.ConstantTimeCompare(hashedPw[:], actualPw) == 1 {
return true
}
}
}
return false
}
func isAuthenticatedUser(from string) bool {
return strings.HasPrefix(from, "user:") || strings.HasPrefix(from, "tag:")
}
func (c *Acl) applies(from string) bool {
// Returns true iff the Acl is applicable for the requesting user
if listContainsString(c.From, "ANYONE") {
return true
}
if listContainsString(c.From, "AUTHENTICATED") && isAuthenticatedUser(from) {
return true
}
return listContainsString(c.From, from)
}
func (c *Acl) findAclEntryThatAllowsRequest(from, method, url string) *AclEntry {
// Check whether an acl permits a request. If so, return the entry that permitted the request.
// 1) The Acl must apply to the requesting user
// 2) The request method and url must match one of the Acl entries
if !c.applies(from) {
return nil
}
for _, e := range c.Entries {
if e.AllMethods || listContainsString(e.Methods, method) {
if e.Pattern.MatchString(url) {
return &e
}
}
}
return nil
}
func (c *Acl) bypassMitm(from, hostAndPort string) bool {
if !listContainsString(c.From, from) {
return false
}
for _, e := range c.MitmBypass {
if e.MatchString(hostAndPort) {
return true
}
}
return false
}
func popModifiers(aclUrl *AclEntry, words *[]string) {
// Take an ACL line and process any modifiers (currently, just "quiet")
n := len(*words)
if n == 0 {
return
}
if (*words)[n-1] == "quiet" {
aclUrl.Quiet = true
*words = (*words)[:n-1]
}
}
func parseAclURLEntry(words []string) (*AclEntry, error) {
// Take a config line like ["url", "GET", "<regexp>"] and turn it into an AclEntry
var aclUrl AclEntry
popModifiers(&aclUrl, &words)
if len(words) < 2 || len(words) > 3 {
return nil, errors.New("wrong number of fields")
}
if words[0] != "url" {
return nil, errors.New("expecting entry to start with 'url'")
}
var urlPattern string
if len(words) == 2 {
// url <regexp>
aclUrl.AllMethods = true
urlPattern = words[1]
} else { // == 3
// url <methodlist> <regexp>
aclUrl.AllMethods = false
aclUrl.Methods = strings.Split(words[1], ",")
urlPattern = words[2]
}
re, err := regexp.Compile(urlPattern)
if err != nil {
return nil, errors.Wrap(err, "failed to parse acl entry, bad url pattern")
}
aclUrl.Pattern = re
return &aclUrl, nil
}
func parseAclS3BucketEntry(words []string) (*AclEntry, error) {
// Take a config line like ["bucket", "s3-bucket-name"] and turn it into an AclEntry
var aclUrl AclEntry
popModifiers(&aclUrl, &words)
if len(words) != 2 {
return nil, errors.New("wrong number of fields")
}
if words[0] != "s3" {
return nil, errors.New("expecting entry to start with 's3'")
}
validBucket, bucketErr := regexp.MatchString(`^[a-z0-9\-]+$`, words[1])
if !validBucket || bucketErr != nil {
return nil, errors.New("invalid s3 bucket name")
}
//
// See: https://docs.aws.amazon.com/general/latest/gr/rande.html
// 26 March 2019:
// -> s3.ap-southeast-2.amazonaws.com [SUPPORTED]
// -> s3-ap-southeast-2.amazonaws.com [SUPPORTED]
// -> s3.dualstack.ap-southeast-2.amazonaws.com [NOT SUPPORTED]
// -> account-id.s3-control.ap-southeast-2.amazonaws.com [NOT SUPPORTED]
// -> account-id.s3-control.dualstack.ap-southeast-2.amazonaws.com [NOT SUPPORTED]
//
s3UrlPattern := `https?\:\/\/(s3[-.][a-z0-9\-]+|s3)\.amazonaws\.com\/%[1]s|https?\:\/\/%[1]s\.(s3[-.][a-z0-9\-]+|s3)\.amazonaws\.com\/`
urlPattern := fmt.Sprintf(s3UrlPattern, words[1])
aclUrl.AllMethods = true
re, err := regexp.Compile(urlPattern)
if err != nil {
return nil, errors.Wrap(err, "failed to parse url pattern")
}
aclUrl.Pattern = re
return &aclUrl, nil
}
func parseAcl(lines []string) (*Acl, error) {
// Take a list of config lines and turn it into an Acl
var aclConfig Acl
for line_no, l := range lines {
l = strings.TrimLeft(l, " \t")
if len(l) == 0 || l[0] == '#' {
continue
}
words := strings.Fields(l)
if words[0] == "from" {
aclConfig.From = append(aclConfig.From, words[1:]...)
} else if words[0] == "url" {
newEntry, err := parseAclURLEntry(words)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("config error at line: %v", line_no+1))
}
aclConfig.Entries = append(aclConfig.Entries, *newEntry)
} else if words[0] == "s3" {
newEntry, err := parseAclS3BucketEntry(words)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("config error at line: %v", line_no+1))
}
aclConfig.Entries = append(aclConfig.Entries, *newEntry)
} else if words[0] == "bypass" {
for _, hostAndPort := range words[1:] {
if strings.IndexRune(hostAndPort, ':') == -1 {
return nil, errors.Errorf("missing port in bypass at line: %v", line_no+1)
}
}
for _, hostAndPortRe := range words[1:] {
re, err := regexp.Compile(hostAndPortRe)
if err != nil {
return nil, errors.Errorf("failed to parse bypass at line: %v", line_no+1)
}
aclConfig.MitmBypass = append(aclConfig.MitmBypass, re)
}
} else {
return nil, errors.Errorf("unknown directive at line: %v", line_no+1)
}
}
return &aclConfig, nil
}
func loadAclFromFile(data []byte) (*Acl, error) {
lines := strings.Split(string(data), "\n")
result, err := parseAcl(lines)
if err != nil {
return nil, errors.Wrapf(err, "error loading acls")
}
return result, nil
}
func loadUsersFromFile(data []byte) ([]UserEntry, error) {
var result []UserEntry
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.Trim(line, " \t")
if len(line) == 0 || line[0] == '#' {
continue
}
fields := strings.Split(line, ":")
username, passwordHash := fields[0], fields[1]
if len(username) == 0 || len(passwordHash) != PasswordHashLen {
// TODO: logging
continue
}
result = append(result, UserEntry{
Username: username,
PasswordHash: passwordHash,
})
}
return result, nil
}
func (proxy *Inkfish) LoadConfigFromDirectory(configDir string) error {
// Load ACLs and passwd entries from a directory
files, err := ioutil.ReadDir(configDir)
if err != nil {
msg := fmt.Sprintf("failed to list config dir: %v", configDir)
return errors.Wrap(err, msg)
}
for _, fi := range files {
ext := filepath.Ext(fi.Name())
if ext != ".conf" && ext != ".passwd" {
continue
}
if fi.Mode() & os.ModeSymlink != 0 {
// Attempt to resolve symbolic link. Any errors and we
// just skip the file.
target, err := filepath.EvalSymlinks(filepath.Join(configDir, fi.Name()))
if err != nil {
continue
}
fi, err = os.Lstat(target)
if err != nil {
continue
}
}
if !fi.Mode().IsRegular() {
continue
}
fullpath := filepath.Join(configDir, fi.Name())
data, err := ioutil.ReadFile(fullpath)
if err != nil {
return errors.Wrapf(err, "failed read config file: %v", fullpath)
}
if ext == ".conf" {
acl, err := loadAclFromFile(data)
if err != nil {
return errors.Wrapf(err, "error in acl file: %v", fullpath)
}
proxy.Acls = append(proxy.Acls, *acl)
log.Println("loaded config file", fullpath)
} else if ext == ".passwd" {
userRecords, err := loadUsersFromFile(data)
if err != nil {
return errors.Wrapf(err, "error in passwd file: %v", fullpath)
}
proxy.Passwd = append(proxy.Passwd, userRecords...)
log.Println("loaded passwd file:", fullpath)
}
}
return nil
}