spanx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/.pairs +13 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Guardfile +13 -0
- data/LICENSE +22 -0
- data/README.md +175 -0
- data/Rakefile +2 -0
- data/bin/spanx +7 -0
- data/conf/spanx-config.yml.example +44 -0
- data/conf/spanx-whitelist.txt.example +2 -0
- data/lib/spanx.rb +38 -0
- data/lib/spanx/actor/analyzer.rb +94 -0
- data/lib/spanx/actor/collector.rb +64 -0
- data/lib/spanx/actor/log_reader.rb +46 -0
- data/lib/spanx/actor/writer.rb +68 -0
- data/lib/spanx/cli.rb +47 -0
- data/lib/spanx/cli/analyze.rb +50 -0
- data/lib/spanx/cli/disable.rb +36 -0
- data/lib/spanx/cli/enable.rb +36 -0
- data/lib/spanx/cli/flush.rb +36 -0
- data/lib/spanx/cli/watch.rb +91 -0
- data/lib/spanx/config.rb +45 -0
- data/lib/spanx/helper.rb +8 -0
- data/lib/spanx/helper/exit.rb +11 -0
- data/lib/spanx/helper/subclassing.rb +31 -0
- data/lib/spanx/helper/timing.rb +9 -0
- data/lib/spanx/ip_checker.rb +5 -0
- data/lib/spanx/logger.rb +47 -0
- data/lib/spanx/notifier/audit_log.rb +18 -0
- data/lib/spanx/notifier/base.rb +22 -0
- data/lib/spanx/notifier/campfire.rb +47 -0
- data/lib/spanx/notifier/email.rb +61 -0
- data/lib/spanx/runner.rb +74 -0
- data/lib/spanx/usage.rb +9 -0
- data/lib/spanx/version.rb +3 -0
- data/lib/spanx/whitelist.rb +31 -0
- data/spanx.gemspec +32 -0
- data/spec/fixtures/access.log.1 +104 -0
- data/spec/fixtures/access.log.bots +7 -0
- data/spec/fixtures/config.yml +10 -0
- data/spec/fixtures/config_with_checks.yml +18 -0
- data/spec/fixtures/whitelist.txt +4 -0
- data/spec/spanx/actor/analyzer_spec.rb +114 -0
- data/spec/spanx/actor/collector_spec.rb +4 -0
- data/spec/spanx/actor/log_reader_spec.rb +68 -0
- data/spec/spanx/actor/writer_spec.rb +63 -0
- data/spec/spanx/config_spec.rb +62 -0
- data/spec/spanx/helper/timing_spec.rb +22 -0
- data/spec/spanx/notifier/base_spec.rb +16 -0
- data/spec/spanx/notifier/campfire_spec.rb +5 -0
- data/spec/spanx/notifier/email_spec.rb +121 -0
- data/spec/spanx/runner_spec.rb +102 -0
- data/spec/spanx/whitelist_spec.rb +66 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/fakeredis.rb +1 -0
- data/spec/support/mail.rb +10 -0
- metadata +302 -0
data/.gitignore
ADDED
data/.pairs
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
pairs:
|
2
|
+
ag: Atasay Gokkaya; atasay
|
3
|
+
km: Kaan Meralan; kaan
|
4
|
+
kg: Konstantin Gredeskoul; kig
|
5
|
+
ph: Paul Henry; paul
|
6
|
+
sf: Sean Flannagan; sean
|
7
|
+
es: Eric Saxby; sax
|
8
|
+
tn: Truong Nguyen; constantx
|
9
|
+
cc: Cihan Cimen; cihan
|
10
|
+
sc: Server Cimen; server
|
11
|
+
email:
|
12
|
+
prefix: pair
|
13
|
+
domain: wanelo.com
|
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@spanx --create
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#^syntax detection
|
3
|
+
|
4
|
+
# A sample Guardfile
|
5
|
+
# More info at https://github.com/guard/guard#readme
|
6
|
+
|
7
|
+
guard 'rspec' do
|
8
|
+
watch(%r{^spanx\.gemspec}) { "spec"}
|
9
|
+
watch(%r{^spec/.+_spec\.rb$})
|
10
|
+
watch(%r{^lib/(.+)\.rb$}) { "spec" }
|
11
|
+
watch('spec/spec_helper.rb') { "spec" }
|
12
|
+
end
|
13
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Konstantin Gredeskoul
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# Spanx
|
2
|
+
|
3
|
+
Spank down IP spam: IP-based rate limiting for web applications behind HTTP server such as nginx or Apache.
|
4
|
+
|
5
|
+
Spanx is a simple Redis-based web request rate limiter, which integrates into any web application simply by monitoring
|
6
|
+
one or more HTTP server access log file(s) in real time (think Apache/nginx access.log).
|
7
|
+
|
8
|
+
Basic flow is as follows:
|
9
|
+
|
10
|
+
* Spanx tails the access.log file(s)
|
11
|
+
* parses out IP addresses of each request
|
12
|
+
* maintains a tally of request counts per IP, and per a time slice.
|
13
|
+
* Spanx is then able to detect when one or more IPs exceed the rate limiting configuration thresholds provided
|
14
|
+
(multiple thresholds are supported).
|
15
|
+
* When such IP is detected, Spanx immediately writes it out into a block-list file (suitable for consumption by nginx or
|
16
|
+
apache, in format eg "deny 127.0.0.1;"), and then
|
17
|
+
* executes a pre-configured command, presumed to reload HTTP server configuration (such as HUP nginx, etc) and activate new blocking rules.
|
18
|
+
|
19
|
+
Spanx additionally supports regular expression based white list file, that can be used to eliminate certain log lines
|
20
|
+
from the consideration (for example, you Googlebot based on User-Agent).
|
21
|
+
|
22
|
+
### Design
|
23
|
+
|
24
|
+
Spanx can be integrated into part of your application, or can run as a standalone ruby app. Spanx requires ruby
|
25
|
+
1.9.3, and it uses ruby threads to work on a few things in parallel.
|
26
|
+
|
27
|
+
Spanx has two main components:
|
28
|
+
|
29
|
+
1. *watcher* is a process that monitors HTTP server log files, and updates Redis periodically with most recent counts.
|
30
|
+
Watcher also writes out the blocked IP file, if blocked IPs are found in Redis database.
|
31
|
+
|
32
|
+
2. *analyzer* is a process that reads up to date information on IP addresses from Redis, and analyzes it. If any rate
|
33
|
+
limit-exceeding IPs are found, it writes them to the Redis DB, with an expiration TTL set.
|
34
|
+
|
35
|
+
If you have only one web server, you can run both watcher and analyzer as a single ruby process.
|
36
|
+
|
37
|
+
If you have multiple web servers, you need to run watcher on each server, and analyzer only once (somewhere).
|
38
|
+
|
39
|
+
### Alerts
|
40
|
+
|
41
|
+
Besides actually writing out IPs to a block list file, Spanx supports notifiers that will be called when a new IP
|
42
|
+
is blocked. Currently supported are audit log notifier (that writes that information to a log file), a Campfire
|
43
|
+
Chat notifier, which will print IP blocking information into your Campfire chat room, and an Email notifier. It is
|
44
|
+
very easy to write additional notifiers.
|
45
|
+
|
46
|
+
## Installation
|
47
|
+
|
48
|
+
Add this line to your application's Gemfile:
|
49
|
+
|
50
|
+
gem 'spanx'
|
51
|
+
|
52
|
+
And then execute:
|
53
|
+
|
54
|
+
$ bundle
|
55
|
+
|
56
|
+
Or install it yourself as:
|
57
|
+
|
58
|
+
$ gem install spanx
|
59
|
+
|
60
|
+
### Dependencies
|
61
|
+
|
62
|
+
Spanx uses the Pause gem to persist state. This depends on Redis to save state and do set logic on the information it finds.
|
63
|
+
|
64
|
+
## Usage
|
65
|
+
|
66
|
+
Spanx has a single executable with several sub-commands. In practice, multiple commands will
|
67
|
+
be run concurrently to do all of the necessary calculations.
|
68
|
+
|
69
|
+
Configuration can be provided via a YAML file (see example), and/or via command line options. Not
|
70
|
+
all configuration can be set via command line. If an option is provided in both YAML file and command line,
|
71
|
+
then latter is chosen.
|
72
|
+
|
73
|
+
|
74
|
+
### watch
|
75
|
+
|
76
|
+
This command watches an HTTP server log file and writes out blocked IPs to a file specified.
|
77
|
+
|
78
|
+
```bash
|
79
|
+
Usage: [bundle exec] spanx watch [options]
|
80
|
+
-f, --file ACCESS_LOG Apache/nginx access log file to scan continuously
|
81
|
+
-z, --analyze Analyze IPs also (as opposed to running `spanx analyze` in another process)
|
82
|
+
-b, --block_file BLOCK_FILE Output file to store NGINX block list
|
83
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
84
|
+
-d, --daemonize Detach from TTY and run as a daemon
|
85
|
+
-g, --debug Log to STDOUT status of execution and some time metrics
|
86
|
+
-r, --run <shell command> Shell command to run anytime blocked ip file changes, for example "sudo pkill -HUP nginx"
|
87
|
+
-w, --whitelist WHITELIST File with newline separated reg exps, to exclude lines from access log
|
88
|
+
-h, --help Show this message
|
89
|
+
```
|
90
|
+
|
91
|
+
### analyze
|
92
|
+
|
93
|
+
Analyzes IPs found by the `watch` command. If an IP exceeds its maximum count for a time
|
94
|
+
period check (as set in the config file), the IP is written into Redis with a TTL defined by the
|
95
|
+
period check.
|
96
|
+
|
97
|
+
```bash
|
98
|
+
Usage: [bundle exec] spanx analyze [options]
|
99
|
+
-a, --audit AUDIT_FILE Historical record of IP blocking decisions
|
100
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
101
|
+
-d, --daemonize
|
102
|
+
-g, --debug Log status to STDOUT
|
103
|
+
-h, --help Show this message
|
104
|
+
```
|
105
|
+
|
106
|
+
### disable
|
107
|
+
|
108
|
+
Disables IP blocking. Note that this only effects the actual writing out
|
109
|
+
of block files, not of IP tracking or analysis. Note that this requires
|
110
|
+
a connection to redis, and thus requires the same config file used in
|
111
|
+
`analyze` and `watch`.
|
112
|
+
|
113
|
+
```bash
|
114
|
+
Usage: [bundle exec] spanx disable [options]
|
115
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
116
|
+
-g, --debug Log status to STDOUT
|
117
|
+
-h, --help Show this message
|
118
|
+
```
|
119
|
+
|
120
|
+
### disable
|
121
|
+
|
122
|
+
Reenables IP blocking if disabled. As with `disable`, the config file is
|
123
|
+
required to connect to redis.
|
124
|
+
|
125
|
+
```bash
|
126
|
+
Usage: [bundle exec] spanx enable [options]
|
127
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
128
|
+
-g, --debug Log status to STDOUT
|
129
|
+
-h, --help Show this message
|
130
|
+
```
|
131
|
+
|
132
|
+
### flush
|
133
|
+
|
134
|
+
This removes the persistence data around current IP blocks. Use this
|
135
|
+
when you want to remove all data around current blocks without (or in
|
136
|
+
addition to) disabling the blocker.
|
137
|
+
|
138
|
+
```bash
|
139
|
+
Usage: [bundle exec] spanx flush [options]
|
140
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
141
|
+
-g, --debug Log status to STDOUT
|
142
|
+
-h, --help Show this message
|
143
|
+
```
|
144
|
+
|
145
|
+
## Examples
|
146
|
+
|
147
|
+
If you have only one load balancer, you may want to centralize all work into a single process, as such:
|
148
|
+
|
149
|
+
```bash
|
150
|
+
$ spanx watch -w /path/to/whitelist -c /path/to/spanx.conf.yml -z -d
|
151
|
+
```
|
152
|
+
|
153
|
+
With multiple load balancers, this may not be desirable. All hosts will need to process their own access
|
154
|
+
log, but a minimum number of hosts should analyze the IP traffic.
|
155
|
+
|
156
|
+
```bash
|
157
|
+
lb1 $ spanx watch -c spanx.conf.yml -r "sudo pkill -HUP nginx" --debug 2>&1 >> /var/log/spanx.watch.log &
|
158
|
+
lb2 $ spanx watch -c spanx.conf.yml -r "sudo pkill -HUP nginx" --debug 2>&1 >> /var/log/spanx.watch.log &
|
159
|
+
|
160
|
+
lb2 $ spanx analyze -c spanx.conf.yml -a spanx.audit.log --debug 2>&1 >> /var/log/spanx.analyze.log &
|
161
|
+
```
|
162
|
+
|
163
|
+
## Contributing
|
164
|
+
|
165
|
+
1. Fork it
|
166
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
167
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
168
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
169
|
+
5. Create new Pull Request
|
170
|
+
|
171
|
+
## Maintainers
|
172
|
+
|
173
|
+
Konstantin Gredeskoul (@kigster) and Eric Saxby (@sax) at Wanelo, Inc (http://github.com/wanelo)
|
174
|
+
|
175
|
+
(c) 2012, All rights reserved.
|
data/Rakefile
ADDED
data/bin/spanx
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
---
|
2
|
+
:access_log: ""
|
3
|
+
:block_file: "block-ips.conf"
|
4
|
+
:audit_file: "spanx-audit.log"
|
5
|
+
:redis:
|
6
|
+
:host: "127.0.0.1"
|
7
|
+
:port: 6379
|
8
|
+
:db: 1
|
9
|
+
:collector:
|
10
|
+
:resolution: 300 # seconds
|
11
|
+
:history: 21600 # seconds
|
12
|
+
:flush_interval: 5
|
13
|
+
:log_reader:
|
14
|
+
:tail_interval: 1
|
15
|
+
:analyzer:
|
16
|
+
:analyze_interval: 20
|
17
|
+
:blocked_ip_notifiers:
|
18
|
+
- "Spanx::Notifier::AuditLog"
|
19
|
+
- "Spanx::Notifier::Campfire"
|
20
|
+
- "Spanx::Notifier::Email"
|
21
|
+
:period_checks:
|
22
|
+
- :period_seconds: 3600
|
23
|
+
:max_allowed: 2000
|
24
|
+
:block_ttl: 7200
|
25
|
+
- :period_seconds: 600
|
26
|
+
:max_allowed: 600
|
27
|
+
:block_ttl: 1200
|
28
|
+
- :period_seconds: 21600
|
29
|
+
:max_allowed: 8000
|
30
|
+
:block_ttl: 64800
|
31
|
+
:writer:
|
32
|
+
:write_interval: 10
|
33
|
+
:campfire:
|
34
|
+
:enabled: true
|
35
|
+
:room_id: 1111
|
36
|
+
:token: aaffdfsdfadfasdfasdfasdf
|
37
|
+
:account: test
|
38
|
+
:email:
|
39
|
+
:enabled: true
|
40
|
+
:to: "everyone@mycompany.com"
|
41
|
+
:from: "spanx@mycompany.com"
|
42
|
+
:password: "s3cVr3p4ssw0rd"
|
43
|
+
:domain: "mycompany.com"
|
44
|
+
:gateway: "smtp.gmail.com"
|
data/lib/spanx.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'pause'
|
3
|
+
require 'spanx/version'
|
4
|
+
require 'spanx/helper'
|
5
|
+
require 'spanx/logger'
|
6
|
+
require 'spanx/config'
|
7
|
+
require 'spanx/usage'
|
8
|
+
|
9
|
+
require 'spanx/ip_checker'
|
10
|
+
|
11
|
+
require 'spanx/cli'
|
12
|
+
require 'spanx/notifier/base'
|
13
|
+
require 'spanx/notifier/campfire'
|
14
|
+
require 'spanx/notifier/audit_log'
|
15
|
+
require 'spanx/notifier/email'
|
16
|
+
|
17
|
+
require 'spanx/actor/log_reader'
|
18
|
+
require 'spanx/actor/collector'
|
19
|
+
require 'spanx/actor/analyzer'
|
20
|
+
require 'spanx/actor/writer'
|
21
|
+
require 'spanx/whitelist'
|
22
|
+
|
23
|
+
require 'spanx/runner'
|
24
|
+
|
25
|
+
module Spanx
|
26
|
+
end
|
27
|
+
|
28
|
+
class String
|
29
|
+
def constantize
|
30
|
+
camel_cased_word = self
|
31
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
|
32
|
+
raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
|
33
|
+
end
|
34
|
+
|
35
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spanx/logger'
|
2
|
+
require 'spanx/helper/timing'
|
3
|
+
require 'spanx/notifier/base'
|
4
|
+
require 'spanx/notifier/campfire'
|
5
|
+
require 'spanx/notifier/audit_log'
|
6
|
+
require 'spanx/notifier/email'
|
7
|
+
|
8
|
+
module Spanx
|
9
|
+
module Actor
|
10
|
+
class Analyzer
|
11
|
+
include Spanx::Helper::Timing
|
12
|
+
|
13
|
+
attr_accessor :config, :notifiers, :blocked_ips
|
14
|
+
|
15
|
+
def initialize config
|
16
|
+
@config = config
|
17
|
+
@audit_file = config[:audit_file]
|
18
|
+
@notifiers = []
|
19
|
+
initialize_notifiers(config) if config[:analyzer][:blocked_ip_notifiers]
|
20
|
+
|
21
|
+
@blocked_ips = []
|
22
|
+
@previously_blocked_ips = []
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
Thread.new do
|
27
|
+
Thread.current[:name] = "analyzer"
|
28
|
+
Logger.log "starting analyzer loop..."
|
29
|
+
loop do
|
30
|
+
analyze_all_ips()
|
31
|
+
sleep config[:analyzer][:analyze_interval]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Look through every IP on the stack. IPs that fulfill a PeriodCheck
|
37
|
+
# are pushed onto a redis block list.
|
38
|
+
def analyze_all_ips
|
39
|
+
return unless Spanx::IPChecker.enabled?
|
40
|
+
|
41
|
+
@previously_blocked_ips = Spanx::IPChecker.blocked_identifiers
|
42
|
+
|
43
|
+
ips = Spanx::IPChecker.tracked_identifiers
|
44
|
+
|
45
|
+
Logger.logging "analyzed #{ips.size} IPs" do
|
46
|
+
ips.each do |ip|
|
47
|
+
blocked_ip = analyze_ip(ip)
|
48
|
+
blocked_ips << blocked_ip if blocked_ip
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Logger.log "blocking [#{blocked_ips.size}] ips" unless blocked_ips.empty?
|
53
|
+
call_notifiers(blocked_ips)
|
54
|
+
blocked_ips.clear
|
55
|
+
end
|
56
|
+
|
57
|
+
# Analyze individual IP for all defined periods. As soon as one
|
58
|
+
# rule is triggered, exit the method
|
59
|
+
def analyze_ip(ip)
|
60
|
+
Spanx::IPChecker.new(ip).analyze
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def initialize_notifiers(config)
|
66
|
+
notifiers_to_initialize = config[:analyzer][:blocked_ip_notifiers]
|
67
|
+
notifiers_to_initialize.each do |class_name|
|
68
|
+
Logger.logging "instantiating notifier #{class_name}" do
|
69
|
+
begin
|
70
|
+
notifier = class_name.constantize.new(config)
|
71
|
+
self.notifiers << notifier
|
72
|
+
rescue => e
|
73
|
+
Logger.log "error instantiating #{class_name}: #{e.inspect}, notifier disabled."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def call_notifiers(blocked_ips)
|
80
|
+
unless notifiers.empty?
|
81
|
+
blocked_ips.reject { |b| @previously_blocked_ips.include?(b.identifier) }.each do |blocked_ip|
|
82
|
+
self.notifiers.each do |notifier|
|
83
|
+
begin
|
84
|
+
notifier.publish(blocked_ip)
|
85
|
+
rescue => e
|
86
|
+
Logger.log "error notifying #{notifier.inspect} about blocked IP #{blocked_ip}: #{e.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|