web_shield 0.1.0 → 0.1.1

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 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