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