web_shield 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7770e88cb028cf1b22ef97d1fe7b4cfdfdacd668
4
- data.tar.gz: 8d6ca04e94a030f8549519dc612b21fb9b44615a
3
+ metadata.gz: 3bb3b5807870d938c333e58039fffd21e6e3a028
4
+ data.tar.gz: f1e43ecd7c806d1995305ad34987ff90dbb93036
5
5
  SHA512:
6
- metadata.gz: 90b662cce0a88025b954d57ee89212574afec1d91f11e98afc25ae301d42e13a0635a5d387f88b0e20e9734c2b2c3f57becf51db42af74dff0415c1116f52142
7
- data.tar.gz: 5abad4f941f28c259ba8acc486fd75d7d975481aff5cce472e5831db775a8fc689591d4686b346b59f9009f71b6b9a1d9553fc975c76f29cdea34c6c6ab10ec8
6
+ metadata.gz: 291f5ad8c073b8bea8ebf8b2cf701d1cfb0bd1e429cc3358d7d0265da1707302595de3032a17334b206795f4e0683ee5f036c662b0451298ba823db3286c5e3d
7
+ data.tar.gz: d857687ca7eada1dbd5994e0f1c3aa1a79f7fb353d5d6de7fbe06390adbccdc4647a25f22fa58e9a0c51c4965adfa0bbcaf5d47c5b18a60a78aeef129913ba62
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
 
11
11
  *.swp
12
+ *.gem
data/README.md CHANGED
@@ -18,7 +18,20 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- See `exmaples/base_config.rb`
21
+
22
+ ```
23
+ config = WebShield::Config.new
24
+ config.store = WebShield::MemoryStore.new # or WebShield::RedisStore.new(redis: Redis.current)
25
+ config.user_parser = Proc.new {|request| request.params[:token] || request.ip }
26
+
27
+ config.build_shield("*", {
28
+ whitelist: %w{127.0.0.1, 192.168.10.1/8},
29
+ blacklist: %w{123.123.123.123 222.222.222.0/8}
30
+ }, WebShield::IPShield)
31
+ config.build_shield "/api/:ver/*", {period: 30, limit: 10}
32
+ ```
33
+
34
+ More see `exmaples/*_config.rb`
22
35
 
23
36
  ### Config options
24
37
 
@@ -26,7 +39,7 @@ See `exmaples/base_config.rb`
26
39
  * `logger`: Logger instance
27
40
  * `user_parser`: 每个请求的限制都是按用户来的
28
41
  * `blocked_response`: 当 block 时返回的内容
29
- * `build_shield`: 构建防御,一个请求过来请会按构建的顺序去一一检查,如果有一个未通过,直接拒绝请求,如果带 skip_shields,当此 shield 被匹配时,如果通过则执行正常的业务,如果不通过则拒绝请求
42
+ * `build_shield`: 构建防御,一个请求过来请会按构建的顺序去一一检查,如果有一个未通过,直接拒绝请求,如果指定了 dictatorial,当此 shield 被匹配时,如果通过则执行正常的业务,如果不通过则拒绝请求
30
43
 
31
44
  ### Build shield options
32
45
 
@@ -38,7 +51,7 @@ See `exmaples/base_config.rb`
38
51
 
39
52
  ## TODO
40
53
 
41
- * UserShield: user whitelist and blacklist
54
+ * Optimize IPShield#ip_in_list?
42
55
  * HoneypotShield:
43
56
  * Auto block request:
44
57
 
@@ -3,6 +3,7 @@
3
3
  require 'pry'
4
4
  require File.expand_path('../base_config', __FILE__)
5
5
  require File.expand_path('../more_shields_config', __FILE__)
6
+ require File.expand_path('../ip_shield_config', __FILE__)
6
7
 
7
8
  class RackBenchmark
8
9
  def initialize(app)
@@ -41,3 +42,9 @@ map '/api/v2' do
41
42
  run app
42
43
  end
43
44
 
45
+ map '/api/ip' do
46
+ use WebShield::Middleware, $ip_config
47
+ run app
48
+ end
49
+
50
+
@@ -0,0 +1,24 @@
1
+ require 'web_shield'
2
+
3
+ config = WebShield::Config.new
4
+ config.store = WebShield::MemoryStore.new # or WebShield::RedisStore.new(redis: Redis.current)
5
+
6
+ config.user_parser = Proc.new {|request| request.params[:token] || request.ip }
7
+
8
+ # filter order
9
+ $ip_shield = config.build_shield(
10
+ "/api/ip*", {
11
+ whitelist: %w{127.0.0.1},
12
+ blacklist: %w{1.1.1.1}
13
+ },
14
+ WebShield::IPShield
15
+ )
16
+
17
+ config.build_shield "/api/ip*", {period: 5, limit: 3}
18
+
19
+ # dynamic add ips
20
+ $ip_shield.push_to_whitelist(%w{192.168.0.0/16 10.0.0.1/16})
21
+ $ip_shield.push_to_blacklist(%w{111.1.1.1/24 1.2.3.4})
22
+
23
+ $ip_config = config
24
+
@@ -8,9 +8,13 @@ module WebShield
8
8
  class Error < StandardError; end
9
9
 
10
10
  autoload :Config, 'web_shield/config'
11
+
11
12
  autoload :MemoryStore, 'web_shield/memory_store'
13
+
12
14
  autoload :Shield, 'web_shield/shield'
13
15
  autoload :ThrottleShield, 'web_shield/throttle_shield'
16
+ autoload :IPShield, 'web_shield/ip_shield'
17
+
14
18
  autoload :Middleware, 'web_shield/middleware'
15
19
 
16
20
  class << self
@@ -8,6 +8,7 @@ module WebShield
8
8
  def initialize
9
9
  @shields = []
10
10
  @shield_counter = 0
11
+ @id_prefix = Time.now.to_f.to_s
11
12
 
12
13
  @user_parser = Proc.new {|req| req.ip }
13
14
  @blocked_response = Proc.new {|req| [429, {}, ['Too Many Requests']] }
@@ -30,9 +31,19 @@ module WebShield
30
31
  end
31
32
  end
32
33
 
34
+ # return shield
33
35
  def build_shield(path_matcher, options, shield_class = ThrottleShield)
34
- shields << shield_class.new(@shield_counter += 1, path_matcher, options, self)
35
- logger.info("Build shield #{shields.last.id} #{path_matcher} #{options}")
36
+ shield_class.new(generate_id, path_matcher, options, self).tap do |shield|
37
+ shields << shield
38
+ logger.info("Build #{shield.shield_name} #{path_matcher} #{options}")
39
+ end
40
+ end
41
+
42
+
43
+ private
44
+
45
+ def generate_id
46
+ "#{@id_prefix}-#{@shield_counter += 1}"
36
47
  end
37
48
  end
38
49
  end
@@ -0,0 +1,73 @@
1
+ require 'digest'
2
+ require 'ipaddr'
3
+
4
+ module WebShield
5
+ class IPShield < Shield
6
+ OPTION_KEYS =[:whitelist, :blacklist]
7
+
8
+ # Params:
9
+ # path:
10
+ # options:
11
+ # whitelist: options, defualt [], like 172.10.10.10 172.10.10.10/16
12
+ # blacklist: options, default [], like 172.10.10.10 172.10.10.10/16
13
+ def initialize(id, shield_path, options, config)
14
+ super
15
+
16
+ check_options(@options)
17
+ @options[:dictatorial] = true
18
+ push_to_whitelist(options[:whitelist]) if options[:whitelist]
19
+ push_to_blacklist(options[:blacklist]) if options[:blacklist]
20
+ end
21
+
22
+ def filter(request)
23
+ req_path = request.path
24
+ return unless path_matcher.match(req_path)
25
+
26
+ if in_blacklist?(request.ip)
27
+ user = config.user_parser.call(request)
28
+ write_log(:info, "Blacklist block '#{user}' #{request.request_method} #{req_path}")
29
+ :block
30
+ elsif in_whitelist?(request.ip)
31
+ write_log(:info, "Whitelist pass '#{user}' #{request.request_method} #{req_path}")
32
+ :pass
33
+ else
34
+ nil
35
+ end
36
+ end
37
+
38
+ def in_whitelist?(ip)
39
+ in_ip_list?(get_store_key('whitelist'), ip)
40
+ end
41
+
42
+ def in_blacklist?(ip)
43
+ in_ip_list?(get_store_key('blacklist'), ip)
44
+ end
45
+
46
+ def push_to_whitelist(ips)
47
+ config.store.push_to_set(get_store_key('whitelist'), ips)
48
+ end
49
+
50
+ def push_to_blacklist(ips)
51
+ config.store.push_to_set(get_store_key('blacklist'), ips)
52
+ end
53
+
54
+
55
+ private
56
+
57
+ # TODO optimize it
58
+ def in_ip_list?(list_key, ip)
59
+ config.store.read_set(list_key).any? {|ip_range| IPAddr.new(ip_range).include?(ip) }
60
+ end
61
+
62
+ def get_store_key(list_name)
63
+ ['web_shield', 'ip_shield', list_name].join('/')
64
+ end
65
+
66
+ def check_options(options)
67
+ options.each do |key, val|
68
+ raise Error, "Invalid shield option '#{key}'" unless OPTION_KEYS.include?(key)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
@@ -1,4 +1,5 @@
1
1
  require 'monitor'
2
+ require 'set'
2
3
 
3
4
  module WebShield
4
5
  class MemoryStore
@@ -26,6 +27,29 @@ module WebShield
26
27
  end
27
28
  end
28
29
 
30
+ def push_to_set(key, vals)
31
+ values = vals.is_a?(Array) ? vals.map(&:to_s) : [vals.to_s]
32
+ @lock.synchronize do
33
+ @data[key] ||= Set.new
34
+ @data[key].merge(values)
35
+ end
36
+ true
37
+ end
38
+
39
+ def del_from_set(key, vals)
40
+ key_data = @data[key]
41
+ return false unless key_data && key_data.is_a?(Set)
42
+ values = vals.is_a?(Array) ? vals.map(&:to_s) : [vals.to_s]
43
+ @lock.synchronize do
44
+ key_data.delete_if {|val| values.include?(val) }
45
+ end
46
+ true
47
+ end
48
+
49
+ def read_set(key)
50
+ @data[key] || Set.new
51
+ end
52
+
29
53
  def reset(key)
30
54
  @data.delete(key.to_sym)
31
55
  end
@@ -3,15 +3,7 @@ require 'digest'
3
3
  module WebShield
4
4
  class Shield
5
5
  attr_reader :id, :shield_path, :path_matcher, :options, :config
6
- OPTION_KEYS =[:period, :limit, :method, :path_sensitive, :dictatorial]
7
-
8
- # Params:
9
- # path:
10
- # options:
11
- # period: required
12
- # limit: required
13
- # method: optional
14
- # path_sensitive: optional, defualt false
6
+
15
7
  def initialize(id, shield_path, options, config)
16
8
  @id = id
17
9
  @shield_path = shield_path
@@ -29,6 +21,18 @@ module WebShield
29
21
  @options[:dictatorial]
30
22
  end
31
23
 
24
+ def write_log(severity, msg)
25
+ case svrt = severity.to_sym
26
+ when :debug, :info, :warn, :error
27
+ config.logger.send(svrt, "#{shield_name} #{msg}")
28
+ else
29
+ raise "Invalid log severity '#{svrt}'"
30
+ end
31
+ end
32
+
33
+ def shield_name
34
+ self.class.name.split('::', 2).last + "\##{id}"
35
+ end
32
36
 
33
37
  private
34
38
 
@@ -45,7 +49,6 @@ module WebShield
45
49
  def hash_to_symbol_keys(hash)
46
50
  hash.each_with_object({}) do |kv, result|
47
51
  key, val = kv[0].to_sym, kv[1]
48
- raise Error, "Invalid shield option '#{key}'" unless OPTION_KEYS.include?(key)
49
52
  result[key] = val
50
53
  end
51
54
  end
@@ -2,6 +2,20 @@ require 'digest'
2
2
 
3
3
  module WebShield
4
4
  class ThrottleShield < Shield
5
+ OPTION_KEYS =[:period, :limit, :method, :path_sensitive, :dictatorial]
6
+
7
+ # Params:
8
+ # path:
9
+ # options:
10
+ # period: required
11
+ # limit: required
12
+ # method: optional
13
+ # path_sensitive: optional, defualt false
14
+ def initialize(id, shield_path, options, config)
15
+ super
16
+
17
+ check_options(@options)
18
+ end
5
19
 
6
20
  def filter(request)
7
21
  req_path = request.path
@@ -10,21 +24,35 @@ module WebShield
10
24
  return if options[:method] && options[:method].to_s.upcase != request.request_method
11
25
 
12
26
  user = config.user_parser.call(request)
13
- store_keys = [id.to_s, user.to_s]
14
- if options[:path_sensitive]
15
- route = [request.request_method, req_path]
16
- else
17
- route = (options[:method] ? [options[:method].to_s.upcase, shield_path] : [shield_path])
18
- end
19
- store_keys << Digest::MD5.hexdigest(route.join('-'))
20
27
 
21
- if @config.store.incr(store_keys.join('-'), options[:period]) <= options[:limit]
28
+ if @config.store.incr(get_store_key(request, user), options[:period]) <= options[:limit]
29
+ write_log(:debug, "Pass '#{user}' #{request.request_method} #{req_path}")
22
30
  :pass
23
31
  else
24
- config.logger.info("[#{id}] Block '#{user}' #{request.request_method} #{req_path}")
32
+ write_log(:info, "Block '#{user}' #{request.request_method} #{req_path}")
25
33
  :block
26
34
  end
27
35
  end
36
+
37
+
38
+ private
39
+
40
+ def get_store_key(request, user)
41
+ keys = ['web_shield', id.to_s, user.to_s]
42
+ route = if options[:path_sensitive]
43
+ [request.request_method, request.path]
44
+ else
45
+ (options[:method] ? [options[:method].to_s.upcase, shield_path] : [shield_path])
46
+ end
47
+ keys << Digest::MD5.hexdigest(route.join('-'))
48
+ keys.join('/')
49
+ end
50
+
51
+ def check_options(options)
52
+ options.each do |key, val|
53
+ raise Error, "Invalid shield option '#{key}'" unless OPTION_KEYS.include?(key)
54
+ end
55
+ end
28
56
  end
29
57
  end
30
58
 
@@ -1,3 +1,3 @@
1
1
  module WebShield
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web_shield
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - jiangzhi.xie
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-10 00:00:00.000000000 Z
11
+ date: 2016-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -98,9 +98,11 @@ files:
98
98
  - bin/setup
99
99
  - examples/base_config.rb
100
100
  - examples/config.ru
101
+ - examples/ip_shield_config.rb
101
102
  - examples/more_shields_config.rb
102
103
  - lib/web_shield.rb
103
104
  - lib/web_shield/config.rb
105
+ - lib/web_shield/ip_shield.rb
104
106
  - lib/web_shield/memory_store.rb
105
107
  - lib/web_shield/middleware.rb
106
108
  - lib/web_shield/shield.rb