spanx 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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}) { "spec"}
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$}) { "spec" }
11
- watch('spec/spec_helper.rb') { "spec" }
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
+ [![Gem Version](https://badge.fury.io/rb/spanx.png)](http://badge.fury.io/rb/spanx)
4
5
  [![Build status](https://secure.travis-ci.org/wanelo/spanx.png)](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"
@@ -13,6 +13,7 @@ require 'spanx/notifier/base'
13
13
  require 'spanx/notifier/campfire'
14
14
  require 'spanx/notifier/audit_log'
15
15
  require 'spanx/notifier/email'
16
+ require 'spanx/notifier/slack'
16
17
 
17
18
  require 'spanx/actor/log_reader'
18
19
  require 'spanx/actor/collector'
@@ -2,6 +2,7 @@ require 'spanx/logger'
2
2
  require 'spanx/helper/timing'
3
3
  require 'spanx/notifier/base'
4
4
  require 'spanx/notifier/campfire'
5
+ require 'spanx/notifier/slack'
5
6
  require 'spanx/notifier/audit_log'
6
7
  require 'spanx/notifier/email'
7
8
 
@@ -3,36 +3,37 @@ require 'file-tail'
3
3
  module Spanx
4
4
  module Actor
5
5
  class LogReader
6
- attr_accessor :file, :queue, :whitelist
6
+ attr_accessor :files, :queue, :whitelist, :threads
7
7
 
8
- def initialize file, queue, interval = 1, whitelist = nil
9
- @file = Spanx::Actor::File.new(file)
10
- @file.interval = interval
11
- @file.backward(0)
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
- Thread.new do
18
- Thread.current[:name] = "log_reader"
19
- Logger.log "tailing the log file #{file.path}...."
20
- self.read do |line|
21
- queue << [line, Time.now.to_i ] if line
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 &block
27
- @file.tail do |line|
28
- block.call(extract_ip(line)) unless whitelist && whitelist.match?(line)
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?
@@ -0,0 +1,7 @@
1
+ require 'reel'
2
+ require 'webmachine'
3
+
4
+ module Spanx
5
+ module API
6
+ end
7
+ end
@@ -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,15 @@
1
+ require 'webmachine'
2
+ require 'spanx'
3
+
4
+ module Spanx
5
+ module API
6
+ module Resources
7
+ class BlockedIps < Webmachine::Resource
8
+ def to_html
9
+ ips = Spanx::IPChecker.rate_limited_identifiers
10
+ JSON.generate(ips)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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
+
@@ -20,9 +20,14 @@ module Spanx
20
20
  private
21
21
 
22
22
  def validate!
23
- error_exit_with_msg("No command given") if args.empty?
23
+ error_exit_with_msg('No command given') if args.empty?
24
24
  @command = args.first
25
- error_exit_with_msg("No command found matching #{@command}") unless Spanx::CLI.subclasses.include?(@command)
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
- require 'spanx/cli/watch'
44
- require 'spanx/cli/analyze'
45
- require 'spanx/cli/disable'
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
@@ -6,11 +6,12 @@ require 'spanx/runner'
6
6
 
7
7
  class Spanx::CLI::Analyze < Spanx::CLI
8
8
 
9
- banner "Usage: spanx analyze [options]"
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 => "-d",
13
- :long => "--daemonize",
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 => "-h",
39
- :long => "--help",
40
- :description => "Show this message",
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
+
@@ -3,7 +3,8 @@ require 'spanx/logger'
3
3
 
4
4
  class Spanx::CLI::Disable < Spanx::CLI
5
5
 
6
- banner "Usage: spanx disable [options]"
6
+ banner 'Usage: spanx disable [options]'
7
+ description 'Disable IP blocking if enabled'
7
8
 
8
9
  option :config_file,
9
10
  :short => '-c CONFIG',
@@ -3,7 +3,8 @@ require 'spanx/logger'
3
3
 
4
4
  class Spanx::CLI::Enable < Spanx::CLI
5
5
 
6
- banner "Usage: spanx enable [options]"
6
+ banner 'Usage: spanx enable [options]'
7
+ description 'Enable IP Blocking, if disabled'
7
8
 
8
9
  option :config_file,
9
10
  :short => '-c CONFIG',
@@ -3,7 +3,20 @@ require 'spanx/logger'
3
3
 
4
4
  class Spanx::CLI::Flush < Spanx::CLI
5
5
 
6
- banner "Usage: spanx flush [options]"
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
- Spanx::IPChecker.unblock_all
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