web_shield 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7770e88cb028cf1b22ef97d1fe7b4cfdfdacd668
4
+ data.tar.gz: 8d6ca04e94a030f8549519dc612b21fb9b44615a
5
+ SHA512:
6
+ metadata.gz: 90b662cce0a88025b954d57ee89212574afec1d91f11e98afc25ae301d42e13a0635a5d387f88b0e20e9734c2b2c3f57becf51db42af74dff0415c1116f52142
7
+ data.tar.gz: 5abad4f941f28c259ba8acc486fd75d7d975481aff5cce472e5831db775a8fc689591d4686b346b59f9009f71b6b9a1d9553fc975c76f29cdea34c6c6ab10ec8
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ *.swp
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --require gem_helper
3
+ --color
4
+
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in web_shield.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 jiangzhi.xie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # WebShield
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'web_shield'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install web_shield
18
+
19
+ ## Usage
20
+
21
+ See `exmaples/base_config.rb`
22
+
23
+ ### Config options
24
+
25
+ * `store`: 请求记录存储位置
26
+ * `logger`: Logger instance
27
+ * `user_parser`: 每个请求的限制都是按用户来的
28
+ * `blocked_response`: 当 block 时返回的内容
29
+ * `build_shield`: 构建防御,一个请求过来请会按构建的顺序去一一检查,如果有一个未通过,直接拒绝请求,如果带 skip_shields,当此 shield 被匹配时,如果通过则执行正常的业务,如果不通过则拒绝请求
30
+
31
+ ### Build shield options
32
+
33
+ * `period`: required, 配合 limit ,在此周期内,如果请求数达到 limit 定义的上限,则 block 此次请求
34
+ * `limit`: required, 周期内请求数上限
35
+ * `method`: optional, default: nil, 匹配 request method, 比如 'get', 'post' 等
36
+ * `dictatorial`: optional, default: false, 如果 request.path 与 rails_routes_matcher 匹配,那么无论是否有 block 此请求,都不再执行之后的 shields
37
+ * `path_sensitive`: optional, default: false, 针对每个 request.path 来设置防护,防止同一个 path 被刷
38
+
39
+ ## TODO
40
+
41
+ * UserShield: user whitelist and blacklist
42
+ * HoneypotShield:
43
+ * Auto block request:
44
+
45
+ ## Development
46
+
47
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
48
+
49
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/web_shield. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
54
+
55
+
56
+ ## License
57
+
58
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
59
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "web_shield"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
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
+ base_path = '/api/v1'
9
+
10
+ # filter order
11
+ config.build_shield "#{base_path}/sessions", {period: 10, limit: 3, dictatorial: true}
12
+ config.build_shield "#{base_path}/callbacks/app_store", {period: 10, limit: 5, dictatorial: true}
13
+ config.build_shield "#{base_path}/callbacks/alipay", {period: 10, limit: 6, dictatorial: true}
14
+ config.build_shield "#{base_path}/callbacks/xyz", {period: 10, limit: 4, dictatorial: true}
15
+ config.build_shield "#{base_path}/users/change_password", {
16
+ period: 15, limit: 3, dictatorial: true, method: :post
17
+ }
18
+ config.build_shield "#{base_path}/*", {period: 10, limit: 3, path_sensitive: true}
19
+ config.build_shield "/api/config", {period: 10, limit: 2}
20
+ config.build_shield "/api/test", {period: 0, limit: 3}
21
+
22
+ $base_config = config
23
+
@@ -0,0 +1,43 @@
1
+ #\ -s webrick
2
+
3
+ require 'pry'
4
+ require File.expand_path('../base_config', __FILE__)
5
+ require File.expand_path('../more_shields_config', __FILE__)
6
+
7
+ class RackBenchmark
8
+ def initialize(app)
9
+ @app = app
10
+ @counter = {}
11
+ end
12
+
13
+ def call(env)
14
+ $req = request = Rack::Request.new(env)
15
+ s = Time.now
16
+ @app.call(env).tap do
17
+ cost = (Time.now - s) * 1000
18
+ path_counter = @counter[request.path] ||= [0, 0]
19
+ path_counter[0] += 1
20
+ path_counter[1] += cost
21
+ puts "#{cost} ms, avg #{path_counter[1] / path_counter[0]} ms"
22
+ end
23
+ end
24
+ end
25
+
26
+ use RackBenchmark
27
+
28
+ app = Proc.new {|env| [200, {'Content-Type' => 'text/html'}, ['Hello WebShield']] }
29
+
30
+ map '/normal' do
31
+ run app
32
+ end
33
+
34
+ map '/api/v1' do
35
+ use WebShield::Middleware, $base_config
36
+ run app
37
+ end
38
+
39
+ map '/api/v2' do
40
+ use WebShield::Middleware, $more_shields_config
41
+ run app
42
+ end
43
+
@@ -0,0 +1,33 @@
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
+ base_path = '/api/v2'
9
+
10
+ 100.times do |i|
11
+ config.build_shield "#{base_path}/#{i}", {
12
+ period: 10, limit: 3, dictatorial: true
13
+ }
14
+ end
15
+
16
+ config.build_shield "#{base_path}/callbacks/app_store", {
17
+ period: 10, limit: 5, dictatorial: true
18
+ }
19
+ config.build_shield "#{base_path}/callbacks/alipay", {
20
+ period: 10, limit: 6, dictatorial: true
21
+ }
22
+ config.build_shield "#{base_path}/callbacks/xyz", {
23
+ period: 10, limit: 4, dictatorial: true
24
+ }
25
+ config.build_shield "#{base_path}/users/change_password", {
26
+ period: 15, limit: 3, dictatorial: true
27
+ }
28
+ config.build_shield "#{base_path}/*", {period: 10, limit: 3}
29
+ config.build_shield "#{base_path}/config", {period: 10, limit: 2}
30
+ config.build_shield "#{base_path}//test", {period: 0, limit: 3}
31
+
32
+ $more_shields_config = config
33
+
@@ -0,0 +1,39 @@
1
+ require 'logger'
2
+
3
+ module WebShield
4
+ class Config
5
+ attr_accessor :store, :user_parser, :blocked_response, :logger
6
+ attr_reader :shields
7
+
8
+ def initialize
9
+ @shields = []
10
+ @shield_counter = 0
11
+
12
+ @user_parser = Proc.new {|req| req.ip }
13
+ @blocked_response = Proc.new {|req| [429, {}, ['Too Many Requests']] }
14
+ @logger = Logger.new($stdout)
15
+ end
16
+
17
+ def store=(store)
18
+ if store.respond_to?(:incr)
19
+ @store = store
20
+ else
21
+ raise Error, 'Invalid store, interface :incr is need'
22
+ end
23
+ end
24
+
25
+ def user_parser=(parser)
26
+ if parser.respond_to?(:call)
27
+ @user_parser = parser
28
+ else
29
+ raise Error, 'Invalid parser, interface :cal is need'
30
+ end
31
+ end
32
+
33
+ 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
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,38 @@
1
+ require 'monitor'
2
+
3
+ module WebShield
4
+ class MemoryStore
5
+ def initialize
6
+ @data = {}
7
+ @lock = Monitor.new
8
+ end
9
+
10
+ def incr(key, period = 0)
11
+ key = key.to_sym
12
+ current_period = period > 0 ? (Time.now.to_i / period) : 0
13
+
14
+ @lock.synchronize do
15
+ if @data[key]
16
+ if @data[key][1] == current_period
17
+ @data[key][0] += 1
18
+ else
19
+ @data[key][1] = current_period
20
+ @data[key][0] = 1
21
+ end
22
+ else
23
+ @data[key] = [1, current_period]
24
+ 1
25
+ end
26
+ end
27
+ end
28
+
29
+ def reset(key)
30
+ @data.delete(key.to_sym)
31
+ end
32
+
33
+ def clear
34
+ @data.clear
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,35 @@
1
+ require 'rack'
2
+
3
+ module WebShield
4
+ class Middleware
5
+ def initialize(app, config)
6
+ @app = app
7
+ @config = config
8
+ end
9
+
10
+ def call(env)
11
+ request = Rack::Request.new(env)
12
+
13
+ @config.shields.each do |shield|
14
+ result, response = shield.filter(request)
15
+
16
+ case result
17
+ when :block
18
+ return @config.blocked_response.call(request)
19
+ when :response
20
+ return response
21
+ when :pass
22
+ if shield.dictatorial?
23
+ # skip other shields
24
+ return @app.call(env)
25
+ end
26
+ else
27
+ # not match
28
+ end
29
+ end
30
+
31
+ @app.call(env)
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,54 @@
1
+ require 'digest'
2
+
3
+ module WebShield
4
+ class Shield
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
15
+ def initialize(id, shield_path, options, config)
16
+ @id = id
17
+ @shield_path = shield_path
18
+ @path_matcher = build_path_matcher(shield_path)
19
+ @options = hash_to_symbol_keys(options)
20
+ @config = config
21
+ end
22
+
23
+ # Returns: :pass, :block, [:response, rack_response], nil
24
+ def filter(request)
25
+ raise Error, "implement me"
26
+ end
27
+
28
+ def dictatorial?
29
+ @options[:dictatorial]
30
+ end
31
+
32
+
33
+ private
34
+
35
+ # Support symbols: :name, (), *
36
+ def build_path_matcher(path)
37
+ regexp_str = Regexp.escape(path)
38
+ regexp_str.gsub!(/\\\((.+)\\\)/, '(\1)?')
39
+ regexp_str.gsub!(/:[^\/\)]+(\)|\/)?/) {|str| "[^/]+#{$1 ? $1 : nil}" }
40
+ regexp_str.gsub!(/\/$/, '/?')
41
+ regexp_str.gsub!("\\*", '.*')
42
+ Regexp.new("\\A#{regexp_str}\\z", 'i')
43
+ end
44
+
45
+ def hash_to_symbol_keys(hash)
46
+ hash.each_with_object({}) do |kv, result|
47
+ key, val = kv[0].to_sym, kv[1]
48
+ raise Error, "Invalid shield option '#{key}'" unless OPTION_KEYS.include?(key)
49
+ result[key] = val
50
+ end
51
+ end
52
+ end
53
+ end
54
+
@@ -0,0 +1,30 @@
1
+ require 'digest'
2
+
3
+ module WebShield
4
+ class ThrottleShield < Shield
5
+
6
+ def filter(request)
7
+ req_path = request.path
8
+ return unless path_matcher.match(req_path)
9
+ return :block if options[:limit] <= 0
10
+ return if options[:method] && options[:method].to_s.upcase != request.request_method
11
+
12
+ 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
+
21
+ if @config.store.incr(store_keys.join('-'), options[:period]) <= options[:limit]
22
+ :pass
23
+ else
24
+ config.logger.info("[#{id}] Block '#{user}' #{request.request_method} #{req_path}")
25
+ :block
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,3 @@
1
+ module WebShield
2
+ VERSION = "0.1.0"
3
+ end
data/lib/web_shield.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "web_shield/version"
2
+
3
+ require 'rack'
4
+ # require 'active_support'
5
+ # require 'active_support/core_ext'
6
+
7
+ module WebShield
8
+ class Error < StandardError; end
9
+
10
+ autoload :Config, 'web_shield/config'
11
+ autoload :MemoryStore, 'web_shield/memory_store'
12
+ autoload :Shield, 'web_shield/shield'
13
+ autoload :ThrottleShield, 'web_shield/throttle_shield'
14
+ autoload :Middleware, 'web_shield/middleware'
15
+
16
+ class << self
17
+ attr_reader :config
18
+
19
+ def configure(&block)
20
+ @config = Config.new
21
+ block.call(@config)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'web_shield/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "web_shield"
8
+ spec.version = WebShield::VERSION
9
+ spec.authors = ["jiangzhi.xie"]
10
+ spec.email = ["xiejiangzhi@gmail.com"]
11
+
12
+ spec.summary = %q{Rack request shield}
13
+ spec.description = %q{Rack request shield}
14
+ spec.homepage = "https://github.com/xjz19901211/web_shield"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "pry"
26
+
27
+ spec.add_dependency "rack"
28
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: web_shield
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jiangzhi.xie
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-07-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Rack request shield
84
+ email:
85
+ - xiejiangzhi@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - bin/console
98
+ - bin/setup
99
+ - examples/base_config.rb
100
+ - examples/config.ru
101
+ - examples/more_shields_config.rb
102
+ - lib/web_shield.rb
103
+ - lib/web_shield/config.rb
104
+ - lib/web_shield/memory_store.rb
105
+ - lib/web_shield/middleware.rb
106
+ - lib/web_shield/shield.rb
107
+ - lib/web_shield/throttle_shield.rb
108
+ - lib/web_shield/version.rb
109
+ - web_shield.gemspec
110
+ homepage: https://github.com/xjz19901211/web_shield
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubyforge_project:
130
+ rubygems_version: 2.4.5.1
131
+ signing_key:
132
+ specification_version: 4
133
+ summary: Rack request shield
134
+ test_files: []