spanx 0.1.1 → 0.3.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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Guardfile +4 -4
- data/README.md +37 -3
- data/conf/spanx-config.yml.example +5 -0
- data/lib/spanx.rb +1 -0
- data/lib/spanx/actor/analyzer.rb +1 -0
- data/lib/spanx/actor/log_reader.rb +18 -17
- data/lib/spanx/api.rb +7 -0
- data/lib/spanx/api/machine.rb +20 -0
- data/lib/spanx/api/resources/blocked_ips.rb +15 -0
- data/lib/spanx/api/resources/unblock_ip.rb +17 -0
- data/lib/spanx/cli.rb +12 -7
- data/lib/spanx/cli/analyze.rb +7 -6
- data/lib/spanx/cli/api.rb +70 -0
- data/lib/spanx/cli/disable.rb +2 -1
- data/lib/spanx/cli/enable.rb +2 -1
- data/lib/spanx/cli/flush.rb +27 -2
- data/lib/spanx/cli/report.rb +77 -0
- data/lib/spanx/cli/watch.rb +18 -11
- data/lib/spanx/helper/exit.rb +6 -2
- data/lib/spanx/helper/subclassing.rb +9 -0
- data/lib/spanx/logger.rb +1 -1
- data/lib/spanx/notifier/slack.rb +42 -0
- data/lib/spanx/runner.rb +1 -1
- data/lib/spanx/usage.rb +11 -7
- data/lib/spanx/version.rb +1 -1
- data/spanx.gemspec +3 -9
- data/spec/spanx/actor/analyzer_spec.rb +5 -5
- data/spec/spanx/actor/log_reader_spec.rb +78 -41
- data/spec/spanx/api/machine_spec.rb +33 -0
- data/spec/spanx/cli/cli_spec.rb +22 -0
- data/spec/spanx/config_spec.rb +2 -2
- data/spec/spanx/notifier/email_spec.rb +1 -1
- data/spec/spanx/notifier/slack_spec.rb +77 -0
- data/spec/spanx/runner_spec.rb +33 -33
- data/spec/spanx/usage_spec.rb +13 -0
- data/spec/spanx/whitelist_spec.rb +24 -24
- data/spec/spec_helper.rb +1 -0
- metadata +46 -111
- data/.pairs +0 -13
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5c017e3394baa42289958507173ac915803057d2
|
4
|
+
data.tar.gz: 207763061ed579e4bc95da27f12c81349e568b0c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c0b1431c0d6dd273e559dcc67a2d6a297b6212e1e7eca50cf61eca24569dd97677fb19548564d6070df087faa819c8d55d966aafec2030d4c460719028dc5a81
|
7
|
+
data.tar.gz: 6439de18b7f3fffadacd21739a63d614faec491e4085e7c85516e64cb0066fb9b4cadbc3401954648ee7e1703c4e92efdaa106595dbd3b25dce8b58a7affddbb
|
data/Gemfile
CHANGED
@@ -2,3 +2,13 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in spanx.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rspec', '~> 2.14'
|
8
|
+
gem 'fakeredis'
|
9
|
+
gem 'timecop'
|
10
|
+
gem 'webmock'
|
11
|
+
gem 'guard-rspec'
|
12
|
+
gem 'rb-fsevent'
|
13
|
+
gem 'webmachine-test'
|
14
|
+
end
|
data/Guardfile
CHANGED
@@ -4,10 +4,10 @@
|
|
4
4
|
# A sample Guardfile
|
5
5
|
# More info at https://github.com/guard/guard#readme
|
6
6
|
|
7
|
-
guard 'rspec' do
|
8
|
-
watch(%r{^spanx\.gemspec}) {
|
7
|
+
guard 'rspec', cmd: 'bundle exec rspec' do
|
8
|
+
watch(%r{^spanx\.gemspec}) { 'spec' }
|
9
9
|
watch(%r{^spec/.+_spec\.rb$})
|
10
|
-
watch(%r{^lib/(.+)\.rb$}) {
|
11
|
-
watch('spec/spec_helper.rb') {
|
10
|
+
watch(%r{^lib/(.+)\.rb$}) { 'spec' }
|
11
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
12
12
|
end
|
13
13
|
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
Spanx
|
2
2
|
=====
|
3
3
|
|
4
|
+
[](http://badge.fury.io/rb/spanx)
|
4
5
|
[](http://travis-ci.org/wanelo/spanx)
|
5
6
|
|
6
7
|
Spank down IP spam: IP-based rate limiting for web applications behind HTTP server such as nginx or Apache.
|
@@ -43,9 +44,7 @@ If you have multiple web servers, you need to run watcher on each server, and an
|
|
43
44
|
### Alerts
|
44
45
|
|
45
46
|
Besides actually writing out IPs to a block list file, Spanx supports notifiers that will be called when a new IP
|
46
|
-
is blocked. Currently supported are audit log notifier (that writes that information to a log file), a Campfire
|
47
|
-
Chat notifier, which will print IP blocking information into your Campfire chat room, and an Email notifier. It is
|
48
|
-
very easy to write additional notifiers.
|
47
|
+
is blocked. Currently supported are audit log notifier (that writes that information to a log file), both a Campfire and Slack chat notifier (which will print IP blocking information into each respective chat room), and an Email notifier. It is very easy to write additional notifiers.
|
49
48
|
|
50
49
|
## Installation
|
51
50
|
|
@@ -146,6 +145,41 @@ Usage: [bundle exec] spanx flush [options]
|
|
146
145
|
-h, --help Show this message
|
147
146
|
```
|
148
147
|
|
148
|
+
### api
|
149
|
+
|
150
|
+
This starts an HTTP server with endpoints for managing blocked ips. Your
|
151
|
+
application (or admin interface) can connect to this, for example.
|
152
|
+
|
153
|
+
```bash
|
154
|
+
Usage: [bundle exec] spanx api [options]
|
155
|
+
-c, --config CONFIG Path to config file (YML) (required)
|
156
|
+
-g, --debug Log status to STDOUT
|
157
|
+
-h, --help Show this message
|
158
|
+
-h, --host Host for the HTTP server to listen on
|
159
|
+
-p, --port Port for the HTTP server to listen on
|
160
|
+
```
|
161
|
+
|
162
|
+
#### Endpoints:
|
163
|
+
|
164
|
+
To retrieve a list of currently blocked ips:
|
165
|
+
|
166
|
+
```
|
167
|
+
GET /ips/blocked
|
168
|
+
[
|
169
|
+
"127.0.0.1",
|
170
|
+
"11.100.193.12"
|
171
|
+
]
|
172
|
+
```
|
173
|
+
|
174
|
+
To unblock a specific ip:
|
175
|
+
|
176
|
+
This will remove the IP from redis and shortly afterwards it will be removed
|
177
|
+
from the nginx block files.
|
178
|
+
|
179
|
+
```
|
180
|
+
DELETE /ips/blocked/11.100.193.12
|
181
|
+
```
|
182
|
+
|
149
183
|
## Examples
|
150
184
|
|
151
185
|
If you have only one load balancer, you may want to centralize all work into a single process, as such:
|
@@ -18,6 +18,7 @@
|
|
18
18
|
- "Spanx::Notifier::AuditLog"
|
19
19
|
- "Spanx::Notifier::Campfire"
|
20
20
|
- "Spanx::Notifier::Email"
|
21
|
+
- "Spanx::Notifier::Slack"
|
21
22
|
:period_checks:
|
22
23
|
- :period_seconds: 3600
|
23
24
|
:max_allowed: 2000
|
@@ -35,6 +36,10 @@
|
|
35
36
|
:room_id: 1111
|
36
37
|
:token: aaffdfsdfadfasdfasdfasdf
|
37
38
|
:account: test
|
39
|
+
:slack:
|
40
|
+
:enabled: true
|
41
|
+
:token: aaffdfsdfadfasdfasdfasdf
|
42
|
+
:base_url: 'https://wanelo.slack.com'
|
38
43
|
:email:
|
39
44
|
:enabled: true
|
40
45
|
:to: "everyone@mycompany.com"
|
data/lib/spanx.rb
CHANGED
data/lib/spanx/actor/analyzer.rb
CHANGED
@@ -3,36 +3,37 @@ require 'file-tail'
|
|
3
3
|
module Spanx
|
4
4
|
module Actor
|
5
5
|
class LogReader
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :files, :queue, :whitelist, :threads
|
7
7
|
|
8
|
-
def initialize
|
9
|
-
@
|
10
|
-
@
|
11
|
-
|
8
|
+
def initialize files, queue, interval = 1, whitelist = nil
|
9
|
+
@files = Array(files).uniq.map { |file| Spanx::Actor::File.new(file) }
|
10
|
+
@files.each do |file|
|
11
|
+
file.interval = interval
|
12
|
+
file.backward(0)
|
13
|
+
end
|
12
14
|
@whitelist = whitelist
|
13
15
|
@queue = queue
|
16
|
+
@threads = []
|
14
17
|
end
|
15
18
|
|
16
19
|
def run
|
17
|
-
|
18
|
-
Thread.
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
files.each_with_index do |file, i|
|
21
|
+
threads << Thread.new do
|
22
|
+
Thread.current[:name] = "log_reader.#{i}"
|
23
|
+
Logger.log "tailing the log file #{file.path}...."
|
24
|
+
self.read(file) do |line|
|
25
|
+
queue << [line, Time.now.to_i] if line
|
26
|
+
end
|
22
27
|
end
|
23
28
|
end
|
24
29
|
end
|
25
30
|
|
26
|
-
def read
|
27
|
-
|
28
|
-
|
31
|
+
def read file
|
32
|
+
file.tail do |line|
|
33
|
+
yield extract_ip(line) unless whitelist && whitelist.match?(line)
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
32
|
-
def close
|
33
|
-
(@file.close if @file) rescue nil
|
34
|
-
end
|
35
|
-
|
36
37
|
def extract_ip line
|
37
38
|
matchers = line.match(/^((\d{1,3}\.?){4})/)
|
38
39
|
matchers[1] unless matchers.nil?
|
data/lib/spanx/api.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'webmachine'
|
2
|
+
|
3
|
+
require 'spanx/api/resources/blocked_ips'
|
4
|
+
require 'spanx/api/resources/unblock_ip'
|
5
|
+
|
6
|
+
module Spanx
|
7
|
+
module API
|
8
|
+
Machine = Webmachine::Application.new do |app|
|
9
|
+
app.routes do
|
10
|
+
# DELETE /ips/blocked/127.0.0.1
|
11
|
+
add ["ips", "blocked", :ip],
|
12
|
+
->(req) { req.method == "DELETE" },
|
13
|
+
Resources::UnblockIP
|
14
|
+
|
15
|
+
# GET /ips/blocked
|
16
|
+
add ["ips", "blocked"], Resources::BlockedIps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Spanx
|
2
|
+
module API
|
3
|
+
module Resources
|
4
|
+
class UnblockIP < Webmachine::Resource
|
5
|
+
def allowed_methods
|
6
|
+
%W[DELETE]
|
7
|
+
end
|
8
|
+
|
9
|
+
def delete_resource
|
10
|
+
Spanx::IPChecker.new(request.path_info[:ip]).unblock
|
11
|
+
JSON.generate({ok: true})
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
data/lib/spanx/cli.rb
CHANGED
@@ -20,9 +20,14 @@ module Spanx
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def validate!
|
23
|
-
error_exit_with_msg(
|
23
|
+
error_exit_with_msg('No command given') if args.empty?
|
24
24
|
@command = args.first
|
25
|
-
|
25
|
+
if !@command.eql?('-h') && !@command.eql?('--help')
|
26
|
+
error_exit_with_msg("No command found matching #{@command}") unless Spanx::CLI.subclasses.include?(@command)
|
27
|
+
else
|
28
|
+
help_exit
|
29
|
+
end
|
30
|
+
|
26
31
|
end
|
27
32
|
|
28
33
|
def generate_config(argv)
|
@@ -35,13 +40,13 @@ module Spanx
|
|
35
40
|
end
|
36
41
|
|
37
42
|
Spanx::Logger.enable if config[:debug]
|
43
|
+
rescue OptionParser::InvalidOption => e
|
44
|
+
error_exit_with_msg "Whoops, #{e.message}"
|
38
45
|
end
|
39
46
|
|
40
47
|
end
|
41
48
|
end
|
42
49
|
|
43
|
-
|
44
|
-
require
|
45
|
-
|
46
|
-
require 'spanx/cli/enable'
|
47
|
-
require 'spanx/cli/flush'
|
50
|
+
Dir.glob("#{File.expand_path('../cli', __FILE__)}/*.rb").each do |file|
|
51
|
+
require file
|
52
|
+
end
|
data/lib/spanx/cli/analyze.rb
CHANGED
@@ -6,11 +6,12 @@ require 'spanx/runner'
|
|
6
6
|
|
7
7
|
class Spanx::CLI::Analyze < Spanx::CLI
|
8
8
|
|
9
|
-
banner
|
9
|
+
banner 'Usage: spanx analyze [options]'
|
10
|
+
description 'Analyze IP traffic and save blocked IPs into Redis'
|
10
11
|
|
11
12
|
option :daemonize,
|
12
|
-
:short =>
|
13
|
-
:long =>
|
13
|
+
:short => '-d',
|
14
|
+
:long => '--daemonize',
|
14
15
|
:boolean => true,
|
15
16
|
:default => false
|
16
17
|
|
@@ -35,9 +36,9 @@ class Spanx::CLI::Analyze < Spanx::CLI
|
|
35
36
|
:required => false
|
36
37
|
|
37
38
|
option :help,
|
38
|
-
:short =>
|
39
|
-
:long =>
|
40
|
-
:description =>
|
39
|
+
:short => '-h',
|
40
|
+
:long => '--help',
|
41
|
+
:description => 'Show this message',
|
41
42
|
:on => :tail,
|
42
43
|
:boolean => true,
|
43
44
|
:show_options => true,
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'mixlib/cli'
|
3
|
+
require 'daemons/daemonize'
|
4
|
+
require 'spanx/logger'
|
5
|
+
require 'spanx/api/machine'
|
6
|
+
|
7
|
+
class Spanx::CLI::Api < Spanx::CLI
|
8
|
+
|
9
|
+
banner 'Usage: spanx api [options]'
|
10
|
+
description 'Start HTTP server for controlling Spanx (experimental)'
|
11
|
+
|
12
|
+
option :daemonize,
|
13
|
+
:short => '-d',
|
14
|
+
:long => '--daemonize',
|
15
|
+
:boolean => true,
|
16
|
+
:default => false
|
17
|
+
|
18
|
+
option :config_file,
|
19
|
+
:short => '-c CONFIG',
|
20
|
+
:long => '--config CONFIG',
|
21
|
+
:description => 'Path to config file (YML)',
|
22
|
+
:required => true
|
23
|
+
|
24
|
+
option :debug,
|
25
|
+
:short => '-g',
|
26
|
+
:long => '--debug',
|
27
|
+
:description => 'Log status to STDOUT',
|
28
|
+
:boolean => true,
|
29
|
+
:required => false,
|
30
|
+
:default => false
|
31
|
+
|
32
|
+
option :host,
|
33
|
+
:short => '-h HOST',
|
34
|
+
:long => '--host HOST',
|
35
|
+
:description => 'Host for the api to listen on.',
|
36
|
+
:default => '127.0.0.1',
|
37
|
+
:required => false
|
38
|
+
|
39
|
+
option :port,
|
40
|
+
:short => '-p PORT',
|
41
|
+
:long => '--port PORT',
|
42
|
+
:description => "Port for the api to listen on.",
|
43
|
+
:default => 6060,
|
44
|
+
:required => false
|
45
|
+
|
46
|
+
option :help,
|
47
|
+
:short => "-h",
|
48
|
+
:long => "--help",
|
49
|
+
:description => "Show this message",
|
50
|
+
:on => :tail,
|
51
|
+
:boolean => true,
|
52
|
+
:show_options => true,
|
53
|
+
:exit => 0
|
54
|
+
|
55
|
+
def run(argv = ARGV)
|
56
|
+
generate_config(argv)
|
57
|
+
|
58
|
+
puts "Starting Spanx API on #{config[:host]}:#{config[:port]}.."
|
59
|
+
|
60
|
+
Daemonize.daemonize if config[:daemonize]
|
61
|
+
|
62
|
+
Spanx::API::Machine.configure do |c|
|
63
|
+
c.port = config[:port]
|
64
|
+
c.ip = config[:host]
|
65
|
+
end
|
66
|
+
|
67
|
+
Spanx::API::Machine.run
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/lib/spanx/cli/disable.rb
CHANGED
data/lib/spanx/cli/enable.rb
CHANGED
data/lib/spanx/cli/flush.rb
CHANGED
@@ -3,7 +3,20 @@ require 'spanx/logger'
|
|
3
3
|
|
4
4
|
class Spanx::CLI::Flush < Spanx::CLI
|
5
5
|
|
6
|
-
banner
|
6
|
+
banner 'Usage: spanx flush [ -a | -i IPADDRESS ] [options]'
|
7
|
+
description 'Remove a specific IP block, or all blocked IPs'
|
8
|
+
|
9
|
+
option :ip,
|
10
|
+
:short => '-i IPADDRESS',
|
11
|
+
:long => '--ip IPADDRESS',
|
12
|
+
:description => 'Unblock specific IP',
|
13
|
+
:required => false
|
14
|
+
|
15
|
+
option :all,
|
16
|
+
:short => '-a',
|
17
|
+
:long => '--all',
|
18
|
+
:description => 'Unblock all IPs',
|
19
|
+
:required => false
|
7
20
|
|
8
21
|
option :config_file,
|
9
22
|
:short => '-c CONFIG',
|
@@ -31,6 +44,18 @@ class Spanx::CLI::Flush < Spanx::CLI
|
|
31
44
|
|
32
45
|
def run(argv = ARGV)
|
33
46
|
generate_config(argv)
|
34
|
-
|
47
|
+
out = ''
|
48
|
+
keys = if config[:ip] =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
|
49
|
+
out << "unblocking ip #{config[:ip]}: "
|
50
|
+
Spanx::IPChecker.new(config[:ip]).unblock
|
51
|
+
elsif config[:all]
|
52
|
+
out << 'unblocking all IPs: ' if config[:debug]
|
53
|
+
Spanx::IPChecker.unblock_all
|
54
|
+
else
|
55
|
+
error_exit_with_msg 'Either -i or -a flag is required now'
|
56
|
+
end
|
57
|
+
out << "deleted #{keys} IPs that matched"
|
58
|
+
puts out if config[:debug]
|
59
|
+
out
|
35
60
|
end
|
36
61
|
end
|