apiphobic-middleware 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE.txt +19 -0
  5. data/README.md +35 -0
  6. data/lib/apiphobic/accept_header.rb +107 -0
  7. data/lib/apiphobic/errors/invalid_accept_header.rb +34 -0
  8. data/lib/apiphobic/errors/invalid_request_body.rb +32 -0
  9. data/lib/apiphobic/errors/invalid_subdomain.rb +33 -0
  10. data/lib/apiphobic/matchers/accept_header.rb +25 -0
  11. data/lib/apiphobic/matchers/subdomain.rb +20 -0
  12. data/lib/apiphobic/middleware/configurable.rb +27 -0
  13. data/lib/apiphobic/middleware/configuration.rb +39 -0
  14. data/lib/apiphobic/middleware/converters/content_type.rb +23 -0
  15. data/lib/apiphobic/middleware/converters/json_api_parameters.rb +27 -0
  16. data/lib/apiphobic/middleware/validators/accept_header.rb +41 -0
  17. data/lib/apiphobic/middleware/validators/authorization_token.rb +35 -0
  18. data/lib/apiphobic/middleware/validators/subdomain.rb +30 -0
  19. data/lib/apiphobic/middleware/version.rb +7 -0
  20. data/lib/apiphobic/requests/accept_header.rb +55 -0
  21. data/lib/apiphobic/requests/authorization_token.rb +136 -0
  22. data/lib/apiphobic/requests/subdomain.rb +23 -0
  23. data/lib/apiphobic/requests/transform_json_api.rb +94 -0
  24. data/lib/apiphobic/responses/invalid.rb +16 -0
  25. data/lib/apiphobic/responses/invalid_accept_header.rb +18 -0
  26. data/lib/apiphobic/responses/invalid_request_body.rb +18 -0
  27. data/lib/apiphobic/responses/invalid_subdomain.rb +18 -0
  28. data/lib/apiphobic/responses/invalid_token.rb +10 -0
  29. data/lib/middleware.rb +3 -0
  30. metadata +178 -0
  31. metadata.gz.sig +2 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9352c3d38356493b40f17954ccfdd450ebaecdebc6dd14619fa8c5697076414d
4
+ data.tar.gz: dde745db2e429e6ff8d4e37504eb2a7bfc286b20d06105aac4e7ecac50183148
5
+ SHA512:
6
+ metadata.gz: 1c85fb802cbcea1cc06aac643a7cf4694220087da8e20ef95e9c6377aa9f8fb00bbad83bc931f8be83f6db6bf7fdbbc643bf1fad450a34162cfbb3673f63aaae
7
+ data.tar.gz: f8b53e044e4d52d894494b60e0c3fc8fa12c397847ef58b09797cdb9256a92e1001a17e9ce65f5fcd471282eb9f49d5bec308a35fcc68890211091582dfd5cd4
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010-2016 The Kompanee, Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # Middleware
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/middleware`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'middleware'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install middleware
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jfelchner/middleware.
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiphobic
4
+ class AcceptHeader
5
+ attr_accessor :raw_accept_header
6
+ attr_reader :application_name
7
+
8
+ def initialize(header, application_name:)
9
+ self.application_name = application_name
10
+ self.raw_accept_header = header || ''
11
+ end
12
+
13
+ def application_name=(other)
14
+ @accept_header_data = nil
15
+ @accept_header_format = nil
16
+
17
+ @application_name = other
18
+ end
19
+
20
+ def version
21
+ accept_header_data[2]
22
+ end
23
+
24
+ def content_type
25
+ accept_header_data[1]
26
+ end
27
+
28
+ def valid?
29
+ !invalid?
30
+ end
31
+
32
+ def invalid?
33
+ accept_header_data.nil?
34
+ end
35
+
36
+ def to_s
37
+ raw_accept_header
38
+ end
39
+
40
+ private
41
+
42
+ def accept_header_data
43
+ @accept_header_data ||= raw_accept_header.match(accept_header_format)
44
+ end
45
+
46
+ def accept_header_format
47
+ @accept_header_format ||= %r{
48
+ ################################################################################
49
+ # BEGINNING
50
+ ################################################################################
51
+
52
+ (?:
53
+ (?<=\A) # the beginning of the accept header
54
+ | # or
55
+ (?<=,) # the beginning of an accept value
56
+ )
57
+
58
+ ################################################################################
59
+ # APPLICATION NAME
60
+ ################################################################################
61
+
62
+ application/vnd\.#{application_name} # the application vendor string
63
+
64
+ ################################################################################
65
+ # SUBRESOURCE
66
+ ################################################################################
67
+
68
+ (?:
69
+ \+ # a literal '+'
70
+ (\w+) # the subresource name
71
+ )?
72
+
73
+ ################################################################################
74
+ # VERSION SECTION
75
+ ################################################################################
76
+
77
+ (?:
78
+ ; # a literal ';'
79
+
80
+ version= # a literal 'version='
81
+ (
82
+ # VERSION DIGIT GROUPINGS SECTION
83
+
84
+ \d+ # one or more digits
85
+ (?:\.\d+){0,2} # one or two groups of digits with preceeding '.'
86
+
87
+ # VERSION BETA SECTION
88
+ (?:
89
+ beta # a literal 'beta'
90
+ (?:\d*) # a optional beta version
91
+ )?
92
+ )
93
+ )?
94
+
95
+ ################################################################################
96
+ # END
97
+ ################################################################################
98
+
99
+ (?:
100
+ (?=\z) # the end of the accept header
101
+ | # or
102
+ (?=,) # the end of an accept value
103
+ )
104
+ }x
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erratum/error'
4
+
5
+ module Apiphobic
6
+ module Errors
7
+ class InvalidAcceptHeader < RuntimeError
8
+ include Erratum::Error
9
+
10
+ attr_accessor :accept_header
11
+
12
+ def http_status
13
+ 400
14
+ end
15
+
16
+ def title
17
+ 'Invalid Accept Header'
18
+ end
19
+
20
+ def detail
21
+ <<~HEREDOC.chomp.tr("\n", ' ')
22
+ The accept header that you passed in the request cannot be parsed, please
23
+ refer to the documentation to verify.
24
+ HEREDOC
25
+ end
26
+
27
+ def source
28
+ {
29
+ 'accept_header' => accept_header,
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erratum/error'
4
+
5
+ module Apiphobic
6
+ module Errors
7
+ class InvalidRequestBody < RuntimeError
8
+ include Erratum::Error
9
+
10
+ attr_accessor :request_body
11
+
12
+ def http_status
13
+ 400
14
+ end
15
+
16
+ def title
17
+ 'Invalid Request Body'
18
+ end
19
+
20
+ def detail
21
+ 'The information you attempted to send in the request cannot be parsed as ' \
22
+ 'a valid JSON document.'
23
+ end
24
+
25
+ def source
26
+ {
27
+ 'request_body' => request_body,
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erratum/error'
4
+
5
+ module Apiphobic
6
+ module Errors
7
+ class InvalidSubdomain < RuntimeError
8
+ include Erratum::Error
9
+
10
+ attr_accessor :http_host
11
+
12
+ def http_status
13
+ 404
14
+ end
15
+
16
+ def title
17
+ 'Invalid Subdomain'
18
+ end
19
+
20
+ def detail
21
+ <<~HEREDOC.chomp.tr("\n", ' ')
22
+ The subdomain you attempted to access is not valid. Please try again.
23
+ HEREDOC
24
+ end
25
+
26
+ def source
27
+ {
28
+ 'http_host' => http_host,
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/accept_header'
4
+
5
+ module Apiphobic
6
+ module Matchers
7
+ class AcceptHeader
8
+ attr_accessor :accept_header,
9
+ :application_name,
10
+ :request
11
+
12
+ def initialize(application_name:)
13
+ self.application_name = application_name
14
+ end
15
+
16
+ def matches?(request)
17
+ self.request = request
18
+ self.accept_header = ::Apiphobic::AcceptHeader.new(request.accept_header,
19
+ application_name: application_name)
20
+
21
+ accept_header.valid?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiphobic
4
+ module Matchers
5
+ class Subdomain
6
+ attr_accessor :allowed_subdomains,
7
+ :request
8
+
9
+ def initialize(allowed_subdomains:)
10
+ self.allowed_subdomains = Array(allowed_subdomains)
11
+ end
12
+
13
+ def matches?(request)
14
+ self.request = request
15
+
16
+ allowed_subdomains.include? request.subdomain
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/middleware/configuration'
4
+
5
+ module Apiphobic
6
+ module Middleware
7
+ module Configurable
8
+ module ClassMethods
9
+ def configure
10
+ yield configuration
11
+ end
12
+
13
+ def configuration
14
+ ::Apiphobic::Middleware::Configuration.instance
15
+ end
16
+ end
17
+
18
+ def self.included(base)
19
+ base.extend(ClassMethods)
20
+ end
21
+
22
+ def configuration
23
+ ::Apiphobic::Middleware::Configuration.instance
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Apiphobic
6
+ module Middleware
7
+ class Configuration
8
+ include Singleton
9
+
10
+ attr_accessor :application_name
11
+ attr_writer :allowed_api_subdomains,
12
+ :allowed_subdomains
13
+
14
+ def to_h
15
+ {
16
+ allowed_api_subdomains: allowed_api_subdomains,
17
+ allowed_subdomains: allowed_subdomains,
18
+ application_name: application_name,
19
+ }
20
+ end
21
+
22
+ def allowed_subdomains
23
+ @allowed_subdomains || ['api']
24
+ end
25
+
26
+ def allowed_api_subdomains
27
+ @allowed_api_subdomains || ['api']
28
+ end
29
+ end
30
+
31
+ def self.configure
32
+ yield configuration
33
+ end
34
+
35
+ def self.configuration
36
+ @configuration ||= Configuration.instance
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiphobic
4
+ module Middleware
5
+ module Converters
6
+ class ContentType
7
+ JSON_API_MIME_TYPE_PATTERN = %r{application/vnd\.api\+json(?=\z|;)}
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ env['CONTENT_TYPE'] = env['CONTENT_TYPE']
15
+ .to_s
16
+ .gsub(JSON_API_MIME_TYPE_PATTERN, 'application/json')
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'apiphobic/requests/transform_json_api'
5
+ require 'apiphobic/responses/invalid_request_body'
6
+
7
+ module Apiphobic
8
+ module Middleware
9
+ module Converters
10
+ class JsonApiParameters
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = Requests::TransformJsonApi.new(env)
17
+
18
+ json_api_request = request.transform
19
+
20
+ @app.call(json_api_request)
21
+ rescue JSON::ParserError
22
+ Responses::InvalidRequestBody.call(env)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/matchers/accept_header'
4
+ require 'apiphobic/matchers/subdomain'
5
+ require 'apiphobic/middleware/configurable'
6
+ require 'apiphobic/requests/accept_header'
7
+ require 'apiphobic/requests/subdomain'
8
+ require 'apiphobic/responses/invalid_accept_header'
9
+
10
+ module Apiphobic
11
+ module Middleware
12
+ module Validators
13
+ class AcceptHeader
14
+ include Configurable
15
+
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ def call(env)
21
+ subdomain_request = Requests::Subdomain.new(env)
22
+ accept_header_request = Requests::AcceptHeader.new(env)
23
+ accept_header = Matchers::AcceptHeader.new(
24
+ application_name: configuration.application_name,
25
+ )
26
+ subdomain = Matchers::Subdomain.new(
27
+ allowed_subdomains: configuration.allowed_api_subdomains,
28
+ )
29
+
30
+ unless subdomain.matches?(subdomain_request) &&
31
+ accept_header.matches?(accept_header_request)
32
+
33
+ return Responses::InvalidAcceptHeader.call(env)
34
+ end
35
+
36
+ @app.call(env)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erratum/errors/authentication/invalid_token'
4
+ require 'apiphobic/requests/authorization_token'
5
+ require 'apiphobic/responses/invalid_token'
6
+ require 'apiphobic/tokens/configuration'
7
+
8
+ module Apiphobic
9
+ module Middleware
10
+ module Validators
11
+ class AuthorizationToken
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ token_request = Requests::AuthorizationToken.new(
18
+ env,
19
+ type: Apiphobic::Tokens.configuration.type,
20
+ private_key: Apiphobic::Tokens.configuration.private_key,
21
+ )
22
+ token = token_request.fetch
23
+
24
+ token.validate!
25
+
26
+ env['X_JSON_WEB_TOKEN'] = token.to_h
27
+
28
+ @app.call(env)
29
+ rescue ::Erratum::Errors::InvalidToken => error
30
+ Responses::InvalidToken.call(env, error: error)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/matchers/subdomain'
4
+ require 'apiphobic/middleware/configurable'
5
+ require 'apiphobic/responses/invalid_subdomain'
6
+
7
+ module Apiphobic
8
+ module Middleware
9
+ module Validators
10
+ class Subdomain
11
+ include Configurable
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ request = Requests::Subdomain.new(env)
19
+ subdomain = Matchers::Subdomain.new(
20
+ allowed_subdomains: configuration.allowed_subdomains,
21
+ )
22
+
23
+ return Responses::InvalidSubdomain.call(env) unless subdomain.matches?(request)
24
+
25
+ @app.call(env)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiphobic
4
+ module Middleware
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/accept_header'
4
+
5
+ module Apiphobic
6
+ module Requests
7
+ class AcceptHeader
8
+ ACCEPT_PARAM_PATTERN = /(?:\A|&)accept=(.+?)(?=\z|&)/
9
+
10
+ attr_accessor :request
11
+
12
+ def initialize(request)
13
+ self.request = request
14
+ end
15
+
16
+ def accept_header
17
+ if accept_header_from_header.valid? || accept_header_from_params.invalid?
18
+ raw_accept_header_from_header
19
+ else
20
+ raw_accept_header_from_params
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def accept_header_from_header
27
+ @accept_header_from_header ||= \
28
+ ::Apiphobic::AcceptHeader.new(raw_accept_header_from_header,
29
+ application_name: /.*?/)
30
+ end
31
+
32
+ def accept_header_from_params
33
+ @accept_header_from_params ||= \
34
+ ::Apiphobic::AcceptHeader.new(raw_accept_header_from_params,
35
+ application_name: /.*?/)
36
+ end
37
+
38
+ def raw_accept_header_from_header
39
+ if request.respond_to?(:headers)
40
+ request.headers['Accept']
41
+ else
42
+ request['HTTP_ACCEPT']
43
+ end
44
+ end
45
+
46
+ def raw_accept_header_from_params
47
+ if request.respond_to?(:params)
48
+ request.params['accept']
49
+ else
50
+ URI.unescape(request['QUERY_STRING'][ACCEPT_PARAM_PATTERN, 1] || '') # rubocop:disable Lint/UriEscapeUnescape
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/tokens/invalid'
4
+ require 'apiphobic/tokens/base64'
5
+ require 'apiphobic/tokens/base64s/null'
6
+ require 'apiphobic/tokens/json_web_token'
7
+ require 'apiphobic/tokens/json_web_tokens/null'
8
+
9
+ module Apiphobic
10
+ module Requests
11
+ class AuthorizationToken
12
+ BASE64_PATTERN = %r{[A-Za-z0-9_/\+\=\-\.]}
13
+ BASE64_TOKEN_HEADER_PATTERN = /\A(?:Basic|Bearer)\s+(.*)\z/
14
+ BASE64_TOKEN_PARAM_NAME = 'token_b64'
15
+ BASE64_TOKEN_PARAM_PATTERN = /(?:\A|&)#{BASE64_TOKEN_PARAM_NAME}=(.*)(?=\z|&)/
16
+
17
+ JSON_WEB_TOKEN_PATTERN = /(#{BASE64_PATTERN}+?\.){4}#{BASE64_PATTERN}+?/
18
+ JSON_WEB_TOKEN_HEADER_PATTERN = /\AToken\s+(.*)\z/
19
+ JSON_WEB_TOKEN_PARAM_NAME = 'token_jwt'
20
+ JSON_WEB_TOKEN_PARAM_PATTERN = /(?:\A|&)#{JSON_WEB_TOKEN_PARAM_NAME}=(.*)(?=\z|&)/
21
+
22
+ attr_accessor :request,
23
+ :private_key,
24
+ :type
25
+
26
+ def initialize(request,
27
+ private_key:,
28
+ type:)
29
+
30
+ self.request = request
31
+ self.private_key = private_key
32
+ self.type = type
33
+ end
34
+
35
+ def fetch
36
+ if (!token_from_header.blank? && token_from_header.valid?) ||
37
+ (token_from_params.blank? || !token_from_params.valid?)
38
+
39
+ token_from_header
40
+ else
41
+ token_from_params
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def token_from_header
48
+ @token_from_header ||= if header_contains_json_web_token?
49
+ retrieve_token(json_web_token_from_header)
50
+ elsif header_contains_base64_token?
51
+ Tokens::Base64.convert(raw_token: base64_token_from_header)
52
+ else
53
+ Tokens::Null.instance
54
+ end
55
+ rescue Erratum::Errors::InvalidToken
56
+ Tokens::Invalid.instance
57
+ end
58
+
59
+ def token_from_params
60
+ @token_from_params ||= if params_contains_json_web_token?
61
+ retrieve_token(json_web_token_from_params)
62
+ elsif params_contains_base64_token?
63
+ Tokens::Base64.convert(raw_token: base64_token_from_params)
64
+ else
65
+ Tokens::Null.instance
66
+ end
67
+ rescue Erratum::Errors::InvalidToken
68
+ Tokens::Invalid.instance
69
+ end
70
+
71
+ def base64_token_from_header
72
+ raw_authorization_header[BASE64_TOKEN_HEADER_PATTERN, 1]
73
+ end
74
+
75
+ def base64_token_from_params
76
+ if request.respond_to?(:params)
77
+ request.params[BASE64_TOKEN_PARAM_NAME] || ''
78
+ else
79
+ request['QUERY_STRING'][BASE64_TOKEN_PARAM_PATTERN, 1] || ''
80
+ end
81
+ end
82
+
83
+ def json_web_token_from_header
84
+ raw_authorization_header[JSON_WEB_TOKEN_HEADER_PATTERN, 1]
85
+ end
86
+
87
+ def json_web_token_from_params
88
+ if request.respond_to?(:params)
89
+ request.params[JSON_WEB_TOKEN_PARAM_NAME] || ''
90
+ else
91
+ request['QUERY_STRING'][JSON_WEB_TOKEN_PARAM_PATTERN, 1] || ''
92
+ end
93
+ end
94
+
95
+ def header_contains_base64_token?
96
+ raw_authorization_header =~ BASE64_TOKEN_HEADER_PATTERN
97
+ end
98
+
99
+ def header_contains_json_web_token?
100
+ raw_authorization_header =~ JSON_WEB_TOKEN_HEADER_PATTERN
101
+ end
102
+
103
+ def params_contains_base64_token?
104
+ if request.respond_to?(:params)
105
+ request.params.has_key?(BASE64_TOKEN_PARAM_NAME)
106
+ else
107
+ request['QUERY_STRING'] =~ BASE64_TOKEN_PARAM_PATTERN
108
+ end
109
+ end
110
+
111
+ def params_contains_json_web_token?
112
+ if request.respond_to?(:params)
113
+ request.params.has_key?(JSON_WEB_TOKEN_PARAM_NAME)
114
+ else
115
+ request['QUERY_STRING'] =~ JSON_WEB_TOKEN_PARAM_PATTERN
116
+ end
117
+ end
118
+
119
+ def raw_authorization_header
120
+ if request.respond_to?(:headers)
121
+ request.headers['HTTP_AUTHORIZATION'] || ''
122
+ else
123
+ request['HTTP_AUTHORIZATION'] || ''
124
+ end
125
+ end
126
+
127
+ def retrieve_token(token)
128
+ Tokens::JsonWebToken.__send__(
129
+ "from_#{type.downcase}",
130
+ token,
131
+ private_key: private_key,
132
+ )
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiphobic
4
+ module Requests
5
+ class Subdomain
6
+ attr_accessor :request
7
+
8
+ def initialize(request)
9
+ self.request = request
10
+ end
11
+
12
+ def subdomain
13
+ @subdomain ||= raw_host[/\A([a-z\-]+)/i, 1]
14
+ end
15
+
16
+ private
17
+
18
+ def raw_host
19
+ request.fetch('HTTP_HOST', '')
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'apple_core/refinements/string'
5
+ require 'apple_core/refinements/hash'
6
+
7
+ module Apiphobic
8
+ module Requests
9
+ class TransformJsonApi
10
+ using ::AppleCore::Refinements::Hash
11
+ using ::AppleCore::Refinements::String
12
+
13
+ attr_accessor :request
14
+
15
+ def initialize(request)
16
+ self.request = request
17
+ end
18
+
19
+ def transform
20
+ request['QUERY_STRING'] = query_string_with_underscored_parameters
21
+
22
+ if has_content? && json?
23
+ request[content_parameter] = request_body_with_underscored_json_keys
24
+ request['CONTENT_LENGTH'] = content_length
25
+ end
26
+
27
+ request
28
+ end
29
+
30
+ private
31
+
32
+ def has_content?
33
+ request['CONTENT_LENGTH'].to_i.positive?
34
+ end
35
+
36
+ def json?
37
+ request['CONTENT_TYPE'] =~ /json/
38
+ end
39
+
40
+ def content_length
41
+ underscored_request_json.bytesize
42
+ end
43
+
44
+ def content_parameter
45
+ request['rack.input'] ? 'rack.input' : 'RACK_INPUT'
46
+ end
47
+
48
+ def request_body_with_underscored_json_keys
49
+ @request_body_with_underscored_json_keys ||= if content_parameter == 'rack.input'
50
+ StringIO.new(underscored_request_json)
51
+ else
52
+ underscored_request_json
53
+ end
54
+ end
55
+
56
+ def query_string_with_underscored_parameters
57
+ @query_string_with_underscored_parameters ||= begin
58
+ return query_string unless query_string.respond_to?(:gsub)
59
+
60
+ query_string.gsub(/(?<=\A|&|\?)[^=&]+/) do |parameter_name|
61
+ unescaped_parameter_name = CGI.unescape(parameter_name)
62
+ underscored_parameter_name = unescaped_parameter_name.underscore
63
+
64
+ CGI.escape(underscored_parameter_name)
65
+ end
66
+ end
67
+ end
68
+
69
+ def underscored_request_json
70
+ JSON.dump(underscored_request_hash)
71
+ end
72
+
73
+ def underscored_request_hash
74
+ request_hash.deep_underscore_keys
75
+ end
76
+
77
+ def request_hash
78
+ JSON.parse(request_body)
79
+ end
80
+
81
+ def request_body
82
+ if request['rack.input']
83
+ request['rack.input'].read
84
+ else
85
+ request['RACK_INPUT'].to_s
86
+ end
87
+ end
88
+
89
+ def query_string
90
+ request['QUERY_STRING']
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module Apiphobic
5
+ module Responses
6
+ class Invalid
7
+ def self.call(_env, error:)
8
+ [
9
+ error.http_status,
10
+ {},
11
+ ["{\"errors\": [#{error.to_json}]}"],
12
+ ]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/errors/invalid_accept_header'
4
+ require 'apiphobic/responses/invalid'
5
+
6
+ module Apiphobic
7
+ module Responses
8
+ class InvalidAcceptHeader < Responses::Invalid
9
+ def self.call(env)
10
+ error ||= Errors::InvalidAcceptHeader.new(
11
+ accept_header: env['HTTP_ACCEPT'],
12
+ )
13
+
14
+ super(env, error: error)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/errors/invalid_request_body'
4
+ require 'apiphobic/responses/invalid'
5
+
6
+ module Apiphobic
7
+ module Responses
8
+ class InvalidRequestBody < Responses::Invalid
9
+ def self.call(env)
10
+ error ||= Errors::InvalidRequestBody.new(
11
+ request_body: (env['rack.input'] || env['RACK_INPUT']).to_s,
12
+ )
13
+
14
+ super(env, error: error)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/errors/invalid_subdomain'
4
+ require 'apiphobic/responses/invalid'
5
+
6
+ module Apiphobic
7
+ module Responses
8
+ class InvalidSubdomain < Responses::Invalid
9
+ def self.call(env)
10
+ error ||= Errors::InvalidSubdomain.new(
11
+ http_host: env['HTTP_HOST'],
12
+ )
13
+
14
+ super(env, error: error)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/responses/invalid'
4
+
5
+ module Apiphobic
6
+ module Responses
7
+ class InvalidToken < Responses::Invalid
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apiphobic/middleware/version'
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apiphobic-middleware
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - thegranddesign
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDqjCCApKgAwIBAgIBATANBgkqhkiG9w0BAQUFADBNMREwDwYDVQQDDAhydWJ5
14
+ Z2VtczEjMCEGCgmSJomT8ixkARkWE2xpdmluZ2hpZ2hvbnRoZWJsb2cxEzARBgoJ
15
+ kiaJk/IsZAEZFgNjb20wHhcNMTcwODAyMjI1OTM1WhcNMTgwODAyMjI1OTM1WjBN
16
+ MREwDwYDVQQDDAhydWJ5Z2VtczEjMCEGCgmSJomT8ixkARkWE2xpdmluZ2hpZ2hv
17
+ bnRoZWJsb2cxEzARBgoJkiaJk/IsZAEZFgNjb20wggEiMA0GCSqGSIb3DQEBAQUA
18
+ A4IBDwAwggEKAoIBAQDtLa7+7p49gW15OgOyRZad/F92iZcMdDjZ2kAxZlviXgVe
19
+ PCtjfdURobH+YMdt++6eRkE25utIFqHyN51Shxfdc21T3fPQe/ZEoMyiJK4tYzbh
20
+ 7VjNJG4ldvKKpS1p7iVz9imnyTxNwb0JaIOsOFCA04T0u6aCQi2acNvAPLviXk0q
21
+ xJ/CKjI4QUTZKVrBt8Q1Egrp2yzmEnSNftDuTbBb8m4vDR+w325CwbKCgycHJ1/g
22
+ YZ3FO76TzJuRVbsYS/bU5XKHVEpkeFmWBqEXsk4DuUIWLa6WZEJcoZf+YP+1pycG
23
+ 7YqSbydpINtEdopD+EEI+g+zNJ4nSI8/eQcQyEjBAgMBAAGjgZQwgZEwCQYDVR0T
24
+ BAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFDWuVrg4ve0vLu71kqiGdyBnzJGV
25
+ MCsGA1UdEQQkMCKBIHJ1YnlnZW1zQGxpdmluZ2hpZ2hvbnRoZWJsb2cuY29tMCsG
26
+ A1UdEgQkMCKBIHJ1YnlnZW1zQGxpdmluZ2hpZ2hvbnRoZWJsb2cuY29tMA0GCSqG
27
+ SIb3DQEBBQUAA4IBAQDJIpHjbBPGiaY4wOHcXlltQ+BMmhWQNh+1fZtyajQd+7Ay
28
+ fv23mO7Mf25Q38gopQlpaODkfxq54Jt8FvQbr5RYRS4j+JEKb75NgrAtehd8USUd
29
+ CiJJGH+yvGNWug9IGZCGX91HIbTsLQ5IUUWQasC5jGP8nxXufUr9xgAJZZenewny
30
+ B2qKu8q1A/kj6cw62RCY7yBmUXxlcJBj8g+JKYAFbYYKUdQSzf50k9IiWLWunJM+
31
+ Y2GAoHKstmfIVhc4XHOPpmTd2o/C29O9oaRgjrkfQEhF/KvJ/PhoV5hvokzsCyI5
32
+ iUeXPfvrGD/itYIBCgk+fnzyQQ4QtE5hTQaWQ3o2
33
+ -----END CERTIFICATE-----
34
+ date: 2018-05-01 00:00:00.000000000 Z
35
+ dependencies:
36
+ - !ruby/object:Gem::Dependency
37
+ name: apple_core
38
+ requirement: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '1.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.0'
50
+ - !ruby/object:Gem::Dependency
51
+ name: apiphobic-tokens
52
+ requirement: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '1.0'
64
+ - !ruby/object:Gem::Dependency
65
+ name: erratum
66
+ requirement: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '3.1'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '3.1'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '3.7'
85
+ type: :development
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '3.7'
92
+ - !ruby/object:Gem::Dependency
93
+ name: rspeckled
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '0.0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: '0.0'
106
+ - !ruby/object:Gem::Dependency
107
+ name: timecop
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: 0.9.0
113
+ type: :development
114
+ prerelease: false
115
+ version_requirements: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: 0.9.0
120
+ description: ''
121
+ email:
122
+ - rubygems@livinghighontheblog.com
123
+ executables: []
124
+ extensions: []
125
+ extra_rdoc_files: []
126
+ files:
127
+ - LICENSE.txt
128
+ - README.md
129
+ - lib/apiphobic/accept_header.rb
130
+ - lib/apiphobic/errors/invalid_accept_header.rb
131
+ - lib/apiphobic/errors/invalid_request_body.rb
132
+ - lib/apiphobic/errors/invalid_subdomain.rb
133
+ - lib/apiphobic/matchers/accept_header.rb
134
+ - lib/apiphobic/matchers/subdomain.rb
135
+ - lib/apiphobic/middleware/configurable.rb
136
+ - lib/apiphobic/middleware/configuration.rb
137
+ - lib/apiphobic/middleware/converters/content_type.rb
138
+ - lib/apiphobic/middleware/converters/json_api_parameters.rb
139
+ - lib/apiphobic/middleware/validators/accept_header.rb
140
+ - lib/apiphobic/middleware/validators/authorization_token.rb
141
+ - lib/apiphobic/middleware/validators/subdomain.rb
142
+ - lib/apiphobic/middleware/version.rb
143
+ - lib/apiphobic/requests/accept_header.rb
144
+ - lib/apiphobic/requests/authorization_token.rb
145
+ - lib/apiphobic/requests/subdomain.rb
146
+ - lib/apiphobic/requests/transform_json_api.rb
147
+ - lib/apiphobic/responses/invalid.rb
148
+ - lib/apiphobic/responses/invalid_accept_header.rb
149
+ - lib/apiphobic/responses/invalid_request_body.rb
150
+ - lib/apiphobic/responses/invalid_subdomain.rb
151
+ - lib/apiphobic/responses/invalid_token.rb
152
+ - lib/middleware.rb
153
+ homepage: ''
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ allowed_push_host: https://rubygems.org
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.7.6
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Middleware to Validate API Requests
178
+ test_files: []
@@ -0,0 +1,2 @@
1
+ {r �+��A"@�F/��yM�n8q+��g�[��6U����FbŬ��r0$�[���.v���F%'�D��0�FغJ~3cϗr��Wt�Y�tu������t��'�F��Dcy�Ϩ�kd`��h� ��Y�C=K�m)����1�l�^J�O����/m_�z.бI�+<卡��*�����B��t%��t�ֹ�յ����Ԁ�.��ٳ%Ÿ
2
+ ����~���Ewն�Y͑BDHd�� 7���'����7