readme-metrics 2.0.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|