Skip to content

Commit af3996f

Browse files
committed
Include IPCat support in Beta™
1 parent ca31bb9 commit af3996f

File tree

9 files changed

+171
-50
lines changed

9 files changed

+171
-50
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The blacklist syntax is similar to that of [gitignore][1]. An optional prefix `!
2727
4. Match Hostname [e.g. `crawl-66-249-66-1.googlebot.com`]
2828
5. Match Hostname RegExp [e.g.: `~ .*\.cox\.net`]
2929
6. Match Geofence [e.g.: `@ 39.377297 -74.451082 (7km)`]
30+
7. Match [ipcat][2] [e.g. `ipcat Cloudflare Inc`]
3031

3132
### Whitelist
3233

@@ -63,5 +64,13 @@ shady.com
6364
~ (.*)\.shady\.com # Block subdomains of shady
6465
```
6566

67+
## IPCat
68+
69+
A rule that begins with "`ipcat `" will be matched with the IPCat database by name.
70+
This rule is currently in development so this syntax and usage may change. Use `*`
71+
to match a multitude of characters like a glob, e.g. `!ipcat Akamai` to whitelist
72+
Akamai requests.
73+
6674

6775
[1]: https://git-scm.com/docs/gitignore
76+
[2]: https://github.com/oftn-oswg/ipcat

admin.go

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func NewAdminHandler(db *ZerodropDB, config *ZerodropConfig) *AdminHandler {
5656
return handler
5757
}
5858

59+
// AdminPageData represents the data served to the admin templates.
5960
type AdminPageData struct {
6061
Error string
6162
Title string
@@ -64,6 +65,7 @@ type AdminPageData struct {
6465
Entries []ZerodropEntry
6566
}
6667

68+
// ServeLogin renders the login page.
6769
func (a *AdminHandler) ServeLogin(w http.ResponseWriter, r *http.Request) {
6870
page := a.Config.Base + "admin/login"
6971
if r.URL.Path != "/login" {
@@ -79,6 +81,7 @@ func (a *AdminHandler) ServeLogin(w http.ResponseWriter, r *http.Request) {
7981
}
8082
}
8183

84+
// ServeNew renders the new entry page.
8285
func (a *AdminHandler) ServeNew(w http.ResponseWriter, r *http.Request) {
8386
if r.Method == "POST" {
8487
err := r.ParseMultipartForm(int64(a.Config.UploadMaxSize))
@@ -181,6 +184,7 @@ func (a *AdminHandler) ServeNew(w http.ResponseWriter, r *http.Request) {
181184
}
182185
}
183186

187+
// ServeMain serves the entry list.
184188
func (a *AdminHandler) ServeMain(w http.ResponseWriter, r *http.Request) {
185189
data := &AdminPageData{Title: "Zerodrop Admin", LoggedIn: true, Config: a.Config}
186190

auth.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ import (
1414
jwt "github.com/dgrijalva/jwt-go"
1515
)
1616

17+
// AuthCredentials represents the secrets required to maintain authentication.
1718
type AuthCredentials struct {
1819
Digest string
1920
Secret []byte
2021
}
2122

23+
// AuthClaims represents the claims of the JWT (JSON Web Token)
2224
type AuthClaims struct {
2325
Auth bool `json:"admin"`
2426
jwt.StandardClaims
2527
}
2628

29+
// AuthHandler wraps another handler with authentication methods and requirements.
2730
type AuthHandler struct {
2831
Success http.Handler
2932
Failure http.Handler

blacklist.go

+80-18
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,33 @@ import (
88
"strconv"
99
"strings"
1010

11+
"github.com/client9/ipcat"
1112
"github.com/oschwald/geoip2-golang"
1213
)
1314

14-
type ZerodropBlacklistItem struct {
15+
// BlacklistContext is a structure used to contain the external data
16+
// used to categorize IP addresses needed for specific rules, like
17+
// the geolocation database used for geofencing or the ipcat database.
18+
type BlacklistContext struct {
19+
GeoDB *geoip2.Reader
20+
IPSet *ipcat.IntervalSet
21+
}
22+
23+
// BlacklistRule is a structure that represents a rule or comment as part
24+
// of a blacklist.
25+
type BlacklistRule struct {
1526
Comment string
1627
Negation bool
1728
All bool
1829
Network *net.IPNet
1930
IP net.IP
2031
Hostname string
2132
Regexp *regexp.Regexp
22-
Geofence *ZerodropGeofence
33+
Geofence *Geofence
34+
IPCat string
2335
}
2436

25-
func (i ZerodropBlacklistItem) String() (value string) {
37+
func (i BlacklistRule) String() (value string) {
2638
if i.Negation {
2739
value += "!"
2840
}
@@ -61,18 +73,23 @@ func (i ZerodropBlacklistItem) String() (value string) {
6173
return
6274
}
6375

76+
if i.IPCat != "" {
77+
value += "ipcat " + i.IPCat
78+
}
79+
6480
if i.Comment != "" {
6581
value += "# " + i.Comment
6682
}
6783

6884
return
6985
}
7086

71-
type ZerodropBlacklist struct {
72-
List []*ZerodropBlacklistItem
87+
// Blacklist is a list of BlacklistRules
88+
type Blacklist struct {
89+
List []*BlacklistRule
7390
}
7491

75-
func (b ZerodropBlacklist) String() string {
92+
func (b Blacklist) String() string {
7693
itemCount := 0
7794

7895
// Stringify items
@@ -106,9 +123,10 @@ var geofenceUnits = map[string]float64{
106123
"ft": 1609.0 / 5280.0,
107124
}
108125

109-
func ParseBlacklist(text string) ZerodropBlacklist {
126+
// ParseBlacklist parses a text blacklist and returns a Blacklist object.
127+
func ParseBlacklist(text string) Blacklist {
110128
lines := strings.Split(text, "\n")
111-
blacklist := ZerodropBlacklist{List: []*ZerodropBlacklistItem{}}
129+
blacklist := Blacklist{List: []*BlacklistRule{}}
112130

113131
for _, line := range lines {
114132
// A line with # serves as a comment.
@@ -123,7 +141,7 @@ func ParseBlacklist(text string) ZerodropBlacklist {
123141
continue
124142
}
125143

126-
item := &ZerodropBlacklistItem{}
144+
item := &BlacklistRule{}
127145

128146
// An optional prefix "!" which negates the pattern;
129147
// any matching address/host excluded by a previous pattern
@@ -141,6 +159,14 @@ func ParseBlacklist(text string) ZerodropBlacklist {
141159
continue
142160
}
143161

162+
// IPCat database query match
163+
if line[:6] == "ipcat " {
164+
ipcat := strings.TrimSpace(line[6:])
165+
item.IPCat = ipcat
166+
blacklist.Add(item)
167+
continue
168+
}
169+
144170
switch line[0] {
145171
case '@':
146172
// An optional prefix "@" indicates a geofencing target.
@@ -203,7 +229,7 @@ func ParseBlacklist(text string) ZerodropBlacklist {
203229
continue
204230
}
205231

206-
item.Geofence = &ZerodropGeofence{
232+
item.Geofence = &Geofence{
207233
Latitude: lat,
208234
Longitude: lng,
209235
Radius: radius,
@@ -251,14 +277,17 @@ func ParseBlacklist(text string) ZerodropBlacklist {
251277
return blacklist
252278
}
253279

254-
func (b *ZerodropBlacklist) Add(item *ZerodropBlacklistItem) {
280+
// Add appends a BlacklistRule to the Blacklist.
281+
func (b *Blacklist) Add(item *BlacklistRule) {
255282
b.List = append(b.List, item)
256283
}
257284

258-
func (b *ZerodropBlacklist) Allow(ip net.IP, geodb *geoip2.Reader) bool {
285+
// Allow decides whether the Blacklist permits the selected IP address.
286+
func (b *Blacklist) Allow(ctx *BlacklistContext, ip net.IP) bool {
259287
allow := true
260288

261-
user := (*ZerodropGeofence)(nil)
289+
user := (*Geofence)(nil)
290+
category := (*ipcat.Interval)(nil)
262291

263292
for _, item := range b.List {
264293
match := false
@@ -311,18 +340,18 @@ func (b *ZerodropBlacklist) Allow(ip net.IP, geodb *geoip2.Reader) bool {
311340
}
312341
}
313342
} else if item.Geofence != nil {
314-
if geodb == nil {
315-
log.Println("Denying access to geofenced blacklist: No GeoDB provided.")
343+
if ctx.GeoDB == nil {
344+
log.Println("Denying access by geofence rule error: no database provided")
316345
return false
317346
}
318347

319348
if user == nil {
320-
record, err := geodb.City(ip)
349+
record, err := ctx.GeoDB.City(ip)
321350
if err != nil {
322-
log.Println("Denying access to geofenced blacklist: Error loading IP.")
351+
log.Printf("Denying access by geofence rule error: %s", err.Error())
323352
return false
324353
}
325-
user = &ZerodropGeofence{
354+
user = &Geofence{
326355
Latitude: record.Location.Latitude,
327356
Longitude: record.Location.Longitude,
328357
Radius: float64(record.Location.AccuracyRadius) * 1000.0, // Convert km to m
@@ -338,8 +367,41 @@ func (b *ZerodropBlacklist) Allow(ip net.IP, geodb *geoip2.Reader) bool {
338367
// Blacklist if user intersects at all with bounds
339368
match = !(boundsIntersect&IsDisjoint != 0)
340369
}
370+
} else if item.IPCat != "" {
371+
if ctx.IPSet == nil {
372+
log.Println("Denying access by ipcat rule error: no database provided")
373+
return false
374+
}
375+
376+
if category == nil {
377+
ipv4 := ip.To4()
378+
if ipv4 != nil {
379+
dots := ipv4.String()
380+
interval, err := ctx.IPSet.Contains(dots)
381+
if err != nil {
382+
log.Printf("Denying access by ipcat rule error: %s", err.Error())
383+
return false
384+
}
385+
category = interval
386+
}
387+
}
388+
389+
if category != nil {
390+
var err error
391+
392+
name := strings.ToLower(category.Name)
393+
search := strings.Replace(regexp.QuoteMeta(strings.ToLower(item.IPCat)), `\*`, `.*`, -1)
394+
match, err = regexp.MatchString(search, name)
395+
if err != nil {
396+
log.Printf("Denying access by ipcat rule error: %s", err.Error())
397+
return false
398+
}
399+
}
400+
401+
return false
341402
}
342403

404+
// TODO: Allow early termination based on negation flags
343405
if match {
344406
allow = item.Negation
345407
}

db.go

+22-15
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ import (
77
"time"
88
)
99

10+
// ZerodropEntry is a page entry.
1011
type ZerodropEntry struct {
1112
db *ZerodropDB
12-
Name string // The request URI used to access this entry
13-
URL string // The URL that this entry references
14-
Filename string // The location of the file in the uploads directory
15-
ContentType string // The MIME type to serve as Content-Type header
16-
Redirect bool // Indicates whether to redirect instead of proxy
17-
Creation time.Time // The time this entry was created
18-
AccessBlacklist ZerodropBlacklist // Blacklist
19-
AccessBlacklistCount int // Number of requests that have been caught by the blacklist
20-
AccessExpire bool // Indicates whether to expire after finite access
21-
AccessExpireCount int // The number of requests on this entry before expiry
22-
AccessCount int // The number of times this has been accessed
23-
AccessTrain bool // Whether training is active
13+
Name string // The request URI used to access this entry
14+
URL string // The URL that this entry references
15+
Filename string // The location of the file in the uploads directory
16+
ContentType string // The MIME type to serve as Content-Type header
17+
Redirect bool // Indicates whether to redirect instead of proxy
18+
Creation time.Time // The time this entry was created
19+
AccessBlacklist Blacklist // Blacklist
20+
AccessBlacklistCount int // Number of requests that have been caught by the blacklist
21+
AccessExpire bool // Indicates whether to expire after finite access
22+
AccessExpireCount int // The number of requests on this entry before expiry
23+
AccessCount int // The number of times this has been accessed
24+
AccessTrain bool // Whether training is active
2425
}
2526

2627
// ZerodropDB represents a database connection.
@@ -29,11 +30,13 @@ type ZerodropDB struct {
2930
mapping map[string]ZerodropEntry
3031
}
3132

33+
// Connect opens a connection to the backend.
3234
func (d *ZerodropDB) Connect() error {
3335
d.mapping = map[string]ZerodropEntry{}
3436
return nil
3537
}
3638

39+
// Get returns the entry with the specified name.
3740
func (d *ZerodropDB) Get(name string) (entry ZerodropEntry, ok bool) {
3841
entry, ok = d.mapping[name]
3942
return
@@ -57,16 +60,19 @@ func (d *ZerodropDB) List() []ZerodropEntry {
5760
return list
5861
}
5962

60-
func (db *ZerodropDB) Create(entry *ZerodropEntry) error {
61-
entry.db = db
62-
db.mapping[entry.Name] = *entry
63+
// Create adds an entry to the database.
64+
func (d *ZerodropDB) Create(entry *ZerodropEntry) error {
65+
entry.db = d
66+
d.mapping[entry.Name] = *entry
6367
return nil
6468
}
6569

70+
// Remove removes an entry from the database.
6671
func (d *ZerodropDB) Remove(name string) {
6772
delete(d.mapping, name)
6873
}
6974

75+
// Clear resets the database by removing all entries.
7076
func (d *ZerodropDB) Clear() {
7177
d.Connect()
7278
}
@@ -76,6 +82,7 @@ func (e *ZerodropEntry) IsExpired() bool {
7682
return e.AccessExpire && (e.AccessCount >= e.AccessExpireCount)
7783
}
7884

85+
// Update saves changes to the entry to the database it belongs to.
7986
func (e *ZerodropEntry) Update() error {
8087
if e.db != nil {
8188
return e.db.Create(e)

geofence.go

+12-5
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ import (
44
"github.com/kellydunn/golang-geo"
55
)
66

7-
type ZerodropGeofence struct {
7+
// Geofence represents a point on the Earth with an accuracy radius.
8+
type Geofence struct {
89
Latitude, Longitude, Radius float64
910
}
1011

11-
type ZerodropIntersection uint
12+
// SetIntersection is a description of the relationship between two sets.
13+
type SetIntersection uint
1214

1315
const (
14-
IsDisjoint ZerodropIntersection = 1 << iota
16+
// IsDisjoint means that the two sets have no common elements.
17+
IsDisjoint SetIntersection = 1 << iota
18+
19+
// IsSubset means the first set is a subset of the second.
1520
IsSubset
21+
22+
// IsSuperset means the second set is a subset of the first.
1623
IsSuperset
1724
)
1825

19-
// Intersection gets the intersection data for two points with accuracy radius
20-
func (mi *ZerodropGeofence) Intersection(tu *ZerodropGeofence) (i ZerodropIntersection) {
26+
// Intersection describes the relationship between two geofences
27+
func (mi *Geofence) Intersection(tu *Geofence) (i SetIntersection) {
2128
miPoint := geo.NewPoint(mi.Latitude, mi.Longitude)
2229
tuPoint := geo.NewPoint(tu.Latitude, tu.Longitude)
2330
distance := miPoint.GreatCircleDistance(tuPoint) * 1000

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type ZerodropConfig struct {
2222
AuthDigest string `default:"11a55ac5de2beb9146e01386dd978a13bb9b99388f5eb52e37f69a32e3d5f11e"`
2323

2424
GeoDB string
25+
IPCat string
2526

2627
UploadDirectory string `default:"."`
2728
UploadPermissions uint32 `default:"0600"`

0 commit comments

Comments
 (0)