readme-metrics 2.0.1 → 2.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +72 -0
- data/Gemfile +9 -9
- data/Gemfile.lock +28 -34
- data/LICENSE +1 -1
- data/Makefile +20 -0
- data/README.md +21 -102
- data/Rakefile +2 -2
- data/bin/console +3 -3
- data/examples/metrics-rails/.gitattributes +7 -0
- data/examples/metrics-rails/.gitignore +20 -0
- data/examples/metrics-rails/.ruby-version +1 -0
- data/examples/metrics-rails/Gemfile +31 -0
- data/examples/metrics-rails/Gemfile.lock +189 -0
- data/examples/metrics-rails/README.md +38 -0
- data/examples/metrics-rails/Rakefile +6 -0
- data/examples/metrics-rails/app/controllers/application_controller.rb +2 -0
- data/examples/metrics-rails/app/controllers/metrics_controller.rb +34 -0
- data/examples/metrics-rails/app/models/application_record.rb +3 -0
- data/examples/metrics-rails/bin/bundle +116 -0
- data/examples/metrics-rails/bin/rails +4 -0
- data/examples/metrics-rails/bin/rake +4 -0
- data/examples/metrics-rails/bin/setup +33 -0
- data/examples/metrics-rails/config/application.rb +52 -0
- data/examples/metrics-rails/config/boot.rb +3 -0
- data/examples/metrics-rails/config/credentials.yml.enc +1 -0
- data/examples/metrics-rails/config/database.yml +25 -0
- data/examples/metrics-rails/config/environment.rb +5 -0
- data/examples/metrics-rails/config/environments/development.rb +56 -0
- data/examples/metrics-rails/config/environments/production.rb +68 -0
- data/examples/metrics-rails/config/environments/test.rb +50 -0
- data/examples/metrics-rails/config/initializers/cors.rb +16 -0
- data/examples/metrics-rails/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/metrics-rails/config/initializers/inflections.rb +16 -0
- data/examples/metrics-rails/config/locales/en.yml +33 -0
- data/examples/metrics-rails/config/puma.rb +43 -0
- data/examples/metrics-rails/config/routes.rb +5 -0
- data/examples/metrics-rails/config.ru +6 -0
- data/examples/metrics-rails/db/seeds.rb +7 -0
- data/examples/metrics-rails/public/robots.txt +1 -0
- data/lib/readme/content_type_helper.rb +6 -6
- data/lib/readme/errors.rb +7 -5
- data/lib/readme/filter.rb +2 -2
- data/lib/readme/har/collection.rb +3 -1
- data/lib/readme/har/request_serializer.rb +14 -14
- data/lib/readme/har/response_serializer.rb +3 -3
- data/lib/readme/har/serializer.rb +12 -10
- data/lib/readme/http_request.rb +18 -9
- data/lib/readme/http_response.rb +7 -7
- data/lib/readme/metrics/version.rb +3 -1
- data/lib/readme/metrics.rb +33 -42
- data/lib/readme/payload.rb +14 -6
- data/lib/readme/request_queue.rb +4 -4
- data/lib/readme/webhook.rb +42 -0
- data/readme-metrics.gemspec +14 -15
- metadata +38 -33
- data/SECURITY.md +0 -12
@@ -3,12 +3,12 @@ module Readme
|
|
3
3
|
# Assumes the includer has a `content_type` method defined.
|
4
4
|
|
5
5
|
JSON_MIME_TYPES = [
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
]
|
6
|
+
'application/json',
|
7
|
+
'application/x-json',
|
8
|
+
'text/json',
|
9
|
+
'text/x-json',
|
10
|
+
'+json'
|
11
|
+
].freeze
|
12
12
|
|
13
13
|
def json?
|
14
14
|
JSON_MIME_TYPES.any? { |mime_type| content_type.include?(mime_type) }
|
data/lib/readme/errors.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Readme
|
2
4
|
class Errors
|
3
|
-
API_KEY_ERROR =
|
4
|
-
REJECT_PARAMS_ERROR =
|
5
|
-
ALLOW_ONLY_ERROR =
|
6
|
-
BUFFER_LENGTH_ERROR =
|
7
|
-
DEVELOPMENT_ERROR =
|
5
|
+
API_KEY_ERROR = 'Missing API Key'
|
6
|
+
REJECT_PARAMS_ERROR = 'The `reject_params` option must be an array of strings'
|
7
|
+
ALLOW_ONLY_ERROR = 'The `allow_only` option must be an array of strings'
|
8
|
+
BUFFER_LENGTH_ERROR = 'The `buffer_length` must be an Integer'
|
9
|
+
DEVELOPMENT_ERROR = 'The `development` option must be a boolean'
|
8
10
|
LOGGER_ERROR = <<~MESSAGE
|
9
11
|
The `logger` option must be class that responds to the following messages:
|
10
12
|
:unkown, :fatal, :error, :warn, :info, :debug, :level
|
data/lib/readme/filter.rb
CHANGED
@@ -15,7 +15,7 @@ module Readme
|
|
15
15
|
def self.redact(rejected_params)
|
16
16
|
rejected_params.each_with_object({}) do |(k, v), hash|
|
17
17
|
# If it's a string then return the length of the redacted field
|
18
|
-
hash[k.to_str] = "[REDACTED#{v.is_a?(String) ? " #{v.length}" :
|
18
|
+
hash[k.to_str] = "[REDACTED#{v.is_a?(String) ? " #{v.length}" : ''}]"
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -65,7 +65,7 @@ module Readme
|
|
65
65
|
|
66
66
|
class FilterArgsError < StandardError
|
67
67
|
def initialize
|
68
|
-
msg =
|
68
|
+
msg = 'Can only supply either reject_params or allow_only, not both.'
|
69
69
|
super(msg)
|
70
70
|
end
|
71
71
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
1
3
|
module Readme
|
2
4
|
module Har
|
3
5
|
class Collection
|
@@ -11,7 +13,7 @@ module Readme
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def to_a
|
14
|
-
filtered_hash.map { |name, value| {name: name, value: value} }
|
16
|
+
filtered_hash.map { |name, value| { name: name, value: value.is_a?(Hash) ? value.to_json : value } }
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'cgi'
|
2
|
+
require 'readme/har/collection'
|
3
|
+
require 'readme/filter'
|
4
4
|
|
5
5
|
module Readme
|
6
6
|
module Har
|
@@ -18,7 +18,7 @@ module Readme
|
|
18
18
|
httpVersion: @request.http_version,
|
19
19
|
headers: Har::Collection.new(@filter, @request.headers).to_a,
|
20
20
|
cookies: Har::Collection.new(@filter, @request.cookies).to_a,
|
21
|
-
postData:
|
21
|
+
postData: post_data,
|
22
22
|
headersSize: -1,
|
23
23
|
bodySize: @request.content_length
|
24
24
|
}.compact
|
@@ -29,14 +29,14 @@ module Readme
|
|
29
29
|
def url
|
30
30
|
url = URI(@request.url)
|
31
31
|
headers = @request.headers
|
32
|
-
forward_proto = headers[
|
33
|
-
forward_host = headers[
|
32
|
+
forward_proto = headers['X-Forwarded-Proto']
|
33
|
+
forward_host = headers['X-Forwarded-Host']
|
34
34
|
url.host = forward_host if forward_host.is_a?(String)
|
35
35
|
url.scheme = forward_proto if forward_proto.is_a?(String)
|
36
36
|
url.to_s
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
39
|
+
def post_data
|
40
40
|
if @request.content_type.nil?
|
41
41
|
nil
|
42
42
|
elsif @request.form_data?
|
@@ -59,22 +59,22 @@ module Readme
|
|
59
59
|
def request_body
|
60
60
|
if @filter.pass_through?
|
61
61
|
pass_through_body
|
62
|
-
elsif
|
62
|
+
elsif form_urlencoded?
|
63
63
|
form_urlencoded_body
|
64
|
-
elsif
|
64
|
+
elsif json?
|
65
65
|
json_body
|
66
66
|
else
|
67
67
|
@request.body
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def
|
72
|
-
[
|
73
|
-
.include?(@request.content_type) || @request.content_type.include?(
|
71
|
+
def json?
|
72
|
+
['application/json', 'application/x-json', 'text/json', 'text/x-json']
|
73
|
+
.include?(@request.content_type) || @request.content_type.include?('+json')
|
74
74
|
end
|
75
75
|
|
76
|
-
def
|
77
|
-
@request.content_type ==
|
76
|
+
def form_urlencoded?
|
77
|
+
@request.content_type == 'application/x-www-form-urlencoded'
|
78
78
|
end
|
79
79
|
|
80
80
|
def json_body
|
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'rack/utils'
|
2
|
+
require 'readme/har/collection'
|
3
3
|
|
4
4
|
module Readme
|
5
5
|
module Har
|
@@ -37,7 +37,7 @@ module Readme
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def empty_content
|
40
|
-
{mimeType:
|
40
|
+
{ mimeType: '', size: 0 }
|
41
41
|
end
|
42
42
|
|
43
43
|
def json_content
|
@@ -1,13 +1,15 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'readme/metrics'
|
5
|
+
require 'readme/har/request_serializer'
|
6
|
+
require 'readme/har/response_serializer'
|
7
|
+
require 'readme/har/collection'
|
6
8
|
|
7
9
|
module Readme
|
8
10
|
module Har
|
9
11
|
class Serializer
|
10
|
-
HAR_VERSION =
|
12
|
+
HAR_VERSION = '1.2'
|
11
13
|
|
12
14
|
def initialize(request, response, start_time, end_time, filter)
|
13
15
|
@http_request = request
|
@@ -17,7 +19,7 @@ module Readme
|
|
17
19
|
@filter = filter
|
18
20
|
end
|
19
21
|
|
20
|
-
def to_json
|
22
|
+
def to_json(*_args)
|
21
23
|
{
|
22
24
|
log: {
|
23
25
|
version: HAR_VERSION,
|
@@ -31,9 +33,9 @@ module Readme
|
|
31
33
|
|
32
34
|
def creator
|
33
35
|
{
|
34
|
-
name:
|
36
|
+
name: 'readme-metrics (ruby)',
|
35
37
|
version: Readme::Metrics::VERSION,
|
36
|
-
comment: "#{
|
38
|
+
comment: "#{RUBY_PLATFORM}/#{RUBY_VERSION}" # arm64-darwin21/2.7.2
|
37
39
|
}
|
38
40
|
end
|
39
41
|
|
@@ -44,7 +46,7 @@ module Readme
|
|
44
46
|
timings: timings,
|
45
47
|
request: request,
|
46
48
|
response: response,
|
47
|
-
startedDateTime: @start_time.iso8601,
|
49
|
+
startedDateTime: @start_time.utc.iso8601(3),
|
48
50
|
time: elapsed_time
|
49
51
|
}
|
50
52
|
]
|
data/lib/readme/http_request.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/request'
|
3
|
+
require_relative 'content_type_helper'
|
4
4
|
|
5
5
|
module Readme
|
6
6
|
class HttpRequest
|
@@ -11,7 +11,7 @@ module Readme
|
|
11
11
|
Rack::HTTP_VERSION,
|
12
12
|
Rack::HTTP_HOST,
|
13
13
|
Rack::HTTP_PORT
|
14
|
-
]
|
14
|
+
].freeze
|
15
15
|
|
16
16
|
def initialize(env)
|
17
17
|
@request = Rack::Request.new(env)
|
@@ -50,7 +50,7 @@ module Readme
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def options?
|
53
|
-
@request.request_method ==
|
53
|
+
@request.request_method == 'OPTIONS'
|
54
54
|
end
|
55
55
|
|
56
56
|
def headers
|
@@ -60,6 +60,7 @@ module Readme
|
|
60
60
|
.to_h
|
61
61
|
.transform_keys { |header| normalize_header_name(header) }
|
62
62
|
.merge unprefixed_headers
|
63
|
+
.merge host_header
|
63
64
|
end
|
64
65
|
|
65
66
|
def body
|
@@ -82,20 +83,28 @@ module Readme
|
|
82
83
|
# Other "headers" like version and host are prefixed with `HTTP_` by Rack but
|
83
84
|
# don't seem to be considered legit HTTP headers.
|
84
85
|
def http_header?(name)
|
85
|
-
name.start_with?(
|
86
|
+
name.start_with?('HTTP') && !HTTP_NON_HEADERS.include?(name)
|
86
87
|
end
|
87
88
|
|
88
89
|
# Headers like `Content-Type: application/json` come into rack like
|
89
90
|
# `"HTTP_CONTENT_TYPE" => "application/json"`.
|
90
91
|
def normalize_header_name(header)
|
91
|
-
header.delete_prefix(
|
92
|
+
header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
|
92
93
|
end
|
93
94
|
|
94
95
|
# These special headers are explicitly _not_ prefixed with HTTP_ in the Rack
|
95
96
|
# env so we need to add them in manually
|
96
97
|
def unprefixed_headers
|
97
|
-
{
|
98
|
-
|
98
|
+
{
|
99
|
+
'Content-Type' => @request.content_type,
|
100
|
+
'Content-Length' => @request.content_length
|
101
|
+
}.compact
|
102
|
+
end
|
103
|
+
|
104
|
+
def host_header
|
105
|
+
{
|
106
|
+
'Host' => @request.host
|
107
|
+
}.compact
|
99
108
|
end
|
100
109
|
end
|
101
110
|
end
|
data/lib/readme/http_response.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/response'
|
3
|
+
require_relative 'content_type_helper'
|
4
4
|
|
5
5
|
module Readme
|
6
6
|
class HttpResponse < SimpleDelegator
|
@@ -13,22 +13,22 @@ module Readme
|
|
13
13
|
def body
|
14
14
|
if raw_body.respond_to?(:rewind)
|
15
15
|
raw_body.rewind
|
16
|
-
content = raw_body.each.
|
16
|
+
content = raw_body.each.sum('')
|
17
17
|
raw_body.rewind
|
18
18
|
|
19
19
|
content
|
20
20
|
else
|
21
|
-
raw_body.each.
|
21
|
+
raw_body.each.sum('')
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
def content_length
|
26
26
|
if empty_body_status?
|
27
27
|
0
|
28
|
-
elsif !headers[
|
28
|
+
elsif !headers['Content-Length']
|
29
29
|
body.bytesize
|
30
30
|
else
|
31
|
-
headers[
|
31
|
+
headers['Content-Length'].to_i
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
data/lib/readme/metrics.rb
CHANGED
@@ -1,33 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'readme/metrics/version'
|
4
|
+
require 'readme/har/serializer'
|
5
|
+
require 'readme/filter'
|
6
|
+
require 'readme/payload'
|
7
|
+
require 'readme/request_queue'
|
8
|
+
require 'readme/errors'
|
9
|
+
require 'readme/http_request'
|
10
|
+
require 'readme/http_response'
|
11
|
+
require 'httparty'
|
12
|
+
require 'logger'
|
12
13
|
|
13
14
|
module Readme
|
14
15
|
class Metrics
|
15
|
-
|
16
|
-
if OS.windows?
|
17
|
-
"windows"
|
18
|
-
elsif OS.mac?
|
19
|
-
"mac"
|
20
|
-
elsif OS.linux?
|
21
|
-
"linux"
|
22
|
-
else
|
23
|
-
"unknown"
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
SDK_NAME = "Readme.io Ruby SDK"
|
28
|
-
PLATFORM = platform
|
16
|
+
SDK_NAME = 'readme-metrics'
|
29
17
|
DEFAULT_BUFFER_LENGTH = 1
|
30
|
-
ENDPOINT =
|
18
|
+
ENDPOINT = URI.join(ENV['README_METRICS_SERVER'] || 'https://metrics.readme.io', '/v1/request')
|
31
19
|
|
32
20
|
def self.logger
|
33
21
|
@@logger
|
@@ -77,15 +65,16 @@ module Readme
|
|
77
65
|
request = HttpRequest.new(env)
|
78
66
|
har = Har::Serializer.new(request, response, start_time, end_time, @filter)
|
79
67
|
user_info = @get_user_info.call(env)
|
68
|
+
ip = env['REMOTE_ADDR']
|
80
69
|
|
81
70
|
if !user_info_valid?(user_info)
|
82
71
|
Readme::Metrics.logger.warn Errors.bad_block_message(user_info)
|
83
72
|
elsif request.options?
|
84
|
-
Readme::Metrics.logger.info
|
73
|
+
Readme::Metrics.logger.info 'OPTIONS request omitted from ReadMe API logging'
|
85
74
|
elsif !can_filter? request, response
|
86
75
|
Readme::Metrics.logger.warn "Request or response body MIME type isn't supported for filtering. Omitting request from ReadMe API logging"
|
87
76
|
else
|
88
|
-
payload = Payload.new(har, user_info, development: @development)
|
77
|
+
payload = Payload.new(har, user_info, ip, development: @development)
|
89
78
|
@@request_queue.push(payload.to_json) unless payload.ignore
|
90
79
|
end
|
91
80
|
end
|
@@ -121,34 +110,36 @@ module Readme
|
|
121
110
|
raise Errors::ConfigurationError, Errors::BUFFER_LENGTH_ERROR
|
122
111
|
end
|
123
112
|
|
124
|
-
if options[:development] && !
|
113
|
+
if options[:development] && !a_boolean?(options[:development])
|
125
114
|
raise Errors::ConfigurationError, Errors::DEVELOPMENT_ERROR
|
126
115
|
end
|
127
116
|
|
128
|
-
if options[:logger] &&
|
117
|
+
if options[:logger] && logger_inferface?(options[:logger])
|
129
118
|
raise Errors::ConfigurationError, Errors::LOGGER_ERROR
|
130
119
|
end
|
131
120
|
end
|
132
121
|
|
133
|
-
def
|
134
|
-
[
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
122
|
+
def logger_inferface?(logger)
|
123
|
+
%i[
|
124
|
+
unknown
|
125
|
+
fatal
|
126
|
+
error
|
127
|
+
warn
|
128
|
+
info
|
129
|
+
debug
|
141
130
|
].any? { |message| !logger.respond_to? message }
|
142
131
|
end
|
143
132
|
|
144
|
-
def
|
145
|
-
|
133
|
+
def a_boolean?(arg)
|
134
|
+
[true, false].include?(arg)
|
146
135
|
end
|
147
136
|
|
137
|
+
# rubocop:disable Style/InverseMethods
|
148
138
|
def user_info_valid?(user_info)
|
149
|
-
!user_info.nil? &&
|
139
|
+
(!user_info.nil? &&
|
150
140
|
!user_info.values.any?(&:nil?) &&
|
151
|
-
user_info.
|
141
|
+
user_info.key?(:api_key)) || user_info.key?(:id)
|
152
142
|
end
|
143
|
+
# rubocop:enable Style/InverseMethods
|
153
144
|
end
|
154
145
|
end
|
data/lib/readme/payload.rb
CHANGED
@@ -1,24 +1,32 @@
|
|
1
|
-
require
|
1
|
+
require 'socket'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
def validate_uuid(uuid)
|
5
|
+
return if uuid.nil?
|
6
|
+
|
7
|
+
uuid.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
8
|
+
end
|
2
9
|
|
3
10
|
module Readme
|
4
11
|
class Payload
|
5
12
|
attr_reader :ignore
|
6
13
|
|
7
|
-
def initialize(har, info, development:)
|
14
|
+
def initialize(har, info, ip_address, development:)
|
8
15
|
@har = har
|
9
16
|
@user_info = info.slice(:id, :label, :email)
|
10
17
|
@user_info[:id] = info[:api_key] unless info[:api_key].nil? # swap api_key for id if api_key is present
|
11
18
|
@log_id = info[:log_id]
|
12
19
|
@ignore = info[:ignore]
|
20
|
+
@ip_address = ip_address
|
13
21
|
@development = development
|
14
|
-
@uuid =
|
22
|
+
@uuid = SecureRandom.uuid
|
15
23
|
end
|
16
24
|
|
17
|
-
def to_json
|
25
|
+
def to_json(*_args)
|
18
26
|
{
|
19
|
-
|
27
|
+
_id: validate_uuid(@log_id) ? @log_id : @uuid,
|
20
28
|
group: @user_info,
|
21
|
-
clientIPAddress:
|
29
|
+
clientIPAddress: @ip_address,
|
22
30
|
development: @development,
|
23
31
|
request: JSON.parse(@har.to_json)
|
24
32
|
}.to_json
|
data/lib/readme/request_queue.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require 'readme/metrics'
|
2
2
|
|
3
3
|
module Readme
|
4
4
|
class RequestQueue
|
@@ -30,8 +30,8 @@ module Readme
|
|
30
30
|
Thread.new do
|
31
31
|
HTTParty.post(
|
32
32
|
Readme::Metrics::ENDPOINT,
|
33
|
-
basic_auth: {username: @api_key, password:
|
34
|
-
headers: {
|
33
|
+
basic_auth: { username: @api_key, password: '' },
|
34
|
+
headers: { 'Content-Type' => 'application/json' },
|
35
35
|
body: to_json(payloads)
|
36
36
|
)
|
37
37
|
end
|
@@ -42,7 +42,7 @@ module Readme
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def to_json(payloads)
|
45
|
-
"[#{payloads.join(
|
45
|
+
"[#{payloads.join(', ')}]"
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'readme/metrics'
|
2
|
+
|
3
|
+
module Readme
|
4
|
+
class MissingSignatureError < ArgumentError
|
5
|
+
def message
|
6
|
+
'Missing Signature'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ExpiredSignatureError < RuntimeError
|
11
|
+
def message
|
12
|
+
'Expired Signature'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class InvalidSignatureError < RuntimeError
|
17
|
+
def message
|
18
|
+
'Invalid Signature'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Webhook
|
23
|
+
def self.verify(body, signature, secret)
|
24
|
+
raise MissingSignatureError unless signature
|
25
|
+
|
26
|
+
parsed = signature.split(',').each_with_object({ time: -1, readme_signature: '' }) do |item, accum|
|
27
|
+
k, v = item.split('=')
|
28
|
+
accum[:time] = v if k.eql? 't'
|
29
|
+
accum[:readme_signature] = v if k.eql? 'v0'
|
30
|
+
end
|
31
|
+
|
32
|
+
# Make sure timestamp is recent to prevent replay attacks
|
33
|
+
thirty_minutes = 30 * 60
|
34
|
+
raise ExpiredSignatureError if Time.now.utc - Time.at(0, parsed[:time].to_i, :millisecond).utc > thirty_minutes
|
35
|
+
|
36
|
+
# Verify the signature is valid
|
37
|
+
unsigned = "#{parsed[:time]}.#{body}"
|
38
|
+
mac = OpenSSL::HMAC.hexdigest('SHA256', secret, unsigned)
|
39
|
+
raise InvalidSignatureError if mac != parsed[:readme_signature]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/readme-metrics.gemspec
CHANGED
@@ -1,31 +1,30 @@
|
|
1
|
-
require_relative
|
1
|
+
require_relative 'lib/readme/metrics/version'
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
4
|
+
spec.name = 'readme-metrics'
|
5
5
|
spec.version = Readme::Metrics::VERSION
|
6
|
-
spec.authors = [
|
7
|
-
spec.email = [
|
8
|
-
spec.license =
|
6
|
+
spec.authors = ['ReadMe']
|
7
|
+
spec.email = ['support@readme.io']
|
8
|
+
spec.license = 'ISC'
|
9
9
|
|
10
10
|
spec.summary = "SDK for Readme's metrics API"
|
11
11
|
spec.description = "Middleware for logging requests to Readme's metrics API"
|
12
|
-
spec.homepage =
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new(
|
12
|
+
spec.homepage = 'https://docs.readme.com/metrics/docs/getting-started-with-api-metrics'
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
14
14
|
|
15
|
-
spec.metadata[
|
16
|
-
spec.metadata[
|
17
|
-
spec.metadata[
|
15
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
16
|
+
spec.metadata['source_code_uri'] = 'https://github.com/readmeio/metrics-sdks/tree/main/packages/ruby'
|
17
|
+
spec.metadata['changelog_uri'] = 'https://github.com/readmeio/metrics-sdks/blob/main/CHANGELOG.md'
|
18
18
|
|
19
19
|
# Specify which files should be added to the gem when it is released.
|
20
20
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
-
spec.files = Dir.chdir(File.expand_path(
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
22
22
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
23
|
end
|
24
|
+
|
24
25
|
# spec.bindir = "exe"
|
25
26
|
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
-
spec.require_paths = [
|
27
|
+
spec.require_paths = ['lib']
|
27
28
|
|
28
|
-
spec.add_runtime_dependency
|
29
|
-
spec.add_runtime_dependency "uuid", "~> 2.3.8"
|
30
|
-
spec.add_runtime_dependency "os", "~> 1.1.4"
|
29
|
+
spec.add_runtime_dependency 'httparty', '~> 0.18'
|
31
30
|
end
|