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 +4 -4
- data/.gitignore +1 -0
- data/README.md +16 -3
- data/examples/config.ru +7 -0
- data/examples/ip_shield_config.rb +24 -0
- data/lib/web_shield.rb +4 -0
- data/lib/web_shield/config.rb +13 -2
- data/lib/web_shield/ip_shield.rb +73 -0
- data/lib/web_shield/memory_store.rb +24 -0
- data/lib/web_shield/shield.rb +13 -10
- data/lib/web_shield/throttle_shield.rb +37 -9
- data/lib/web_shield/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bb3b5807870d938c333e58039fffd21e6e3a028
|
4
|
+
data.tar.gz: f1e43ecd7c806d1995305ad34987ff90dbb93036
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 291f5ad8c073b8bea8ebf8b2cf701d1cfb0bd1e429cc3358d7d0265da1707302595de3032a17334b206795f4e0683ee5f036c662b0451298ba823db3286c5e3d
|
7
|
+
data.tar.gz: d857687ca7eada1dbd5994e0f1c3aa1a79f7fb353d5d6de7fbe06390adbccdc4647a25f22fa58e9a0c51c4965adfa0bbcaf5d47c5b18a60a78aeef129913ba62
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -18,7 +18,20 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
|
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`:
|
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
|
-
*
|
54
|
+
* Optimize IPShield#ip_in_list?
|
42
55
|
* HoneypotShield:
|
43
56
|
* Auto block request:
|
44
57
|
|
data/examples/config.ru
CHANGED
@@ -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
|
+
|
data/lib/web_shield.rb
CHANGED
@@ -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
|
data/lib/web_shield/config.rb
CHANGED
@@ -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
|
-
|
35
|
-
|
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
|
data/lib/web_shield/shield.rb
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
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
|
|
data/lib/web_shield/version.rb
CHANGED
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.
|
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-
|
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
|