apiphobic-middleware 1.0.0

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