bugsnag-maze-runner 7.22.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bugsnag-print-load-paths +6 -0
  3. data/bin/download-logs +74 -0
  4. data/bin/maze-runner +174 -0
  5. data/bin/upload-app +56 -0
  6. data/lib/features/scripts/await-android-emulator.sh +11 -0
  7. data/lib/features/scripts/clear-android-app-data.sh +8 -0
  8. data/lib/features/scripts/force-stop-android-app.sh +8 -0
  9. data/lib/features/scripts/install-android-app.sh +15 -0
  10. data/lib/features/scripts/launch-android-app.sh +38 -0
  11. data/lib/features/scripts/launch-android-emulator.sh +15 -0
  12. data/lib/features/steps/android_steps.rb +51 -0
  13. data/lib/features/steps/app_automator_steps.rb +228 -0
  14. data/lib/features/steps/aws_sam_steps.rb +212 -0
  15. data/lib/features/steps/breadcrumb_steps.rb +80 -0
  16. data/lib/features/steps/browser_steps.rb +93 -0
  17. data/lib/features/steps/build_api_steps.rb +25 -0
  18. data/lib/features/steps/document_server_steps.rb +7 -0
  19. data/lib/features/steps/error_reporting_steps.rb +358 -0
  20. data/lib/features/steps/feature_flag_steps.rb +190 -0
  21. data/lib/features/steps/header_steps.rb +72 -0
  22. data/lib/features/steps/log_steps.rb +29 -0
  23. data/lib/features/steps/multipart_request_steps.rb +142 -0
  24. data/lib/features/steps/network_steps.rb +135 -0
  25. data/lib/features/steps/payload_steps.rb +257 -0
  26. data/lib/features/steps/proxy_steps.rb +34 -0
  27. data/lib/features/steps/query_parameter_steps.rb +31 -0
  28. data/lib/features/steps/request_assertion_steps.rb +186 -0
  29. data/lib/features/steps/runner_steps.rb +428 -0
  30. data/lib/features/steps/session_tracking_steps.rb +116 -0
  31. data/lib/features/steps/trace_steps.rb +206 -0
  32. data/lib/features/steps/value_steps.rb +119 -0
  33. data/lib/features/support/env.rb +7 -0
  34. data/lib/features/support/internal_hooks.rb +207 -0
  35. data/lib/maze/api/appium/file_manager.rb +29 -0
  36. data/lib/maze/appium_server.rb +112 -0
  37. data/lib/maze/assertions/request_set_assertions.rb +97 -0
  38. data/lib/maze/aws/sam.rb +112 -0
  39. data/lib/maze/aws_public_ip.rb +53 -0
  40. data/lib/maze/bugsnag_config.rb +42 -0
  41. data/lib/maze/checks/assert_check.rb +69 -0
  42. data/lib/maze/checks/noop_check.rb +34 -0
  43. data/lib/maze/client/appium/base_client.rb +131 -0
  44. data/lib/maze/client/appium/bb_client.rb +102 -0
  45. data/lib/maze/client/appium/bb_devices.rb +127 -0
  46. data/lib/maze/client/appium/bs_client.rb +91 -0
  47. data/lib/maze/client/appium/bs_devices.rb +141 -0
  48. data/lib/maze/client/appium/bs_legacy_client.rb +31 -0
  49. data/lib/maze/client/appium/local_client.rb +67 -0
  50. data/lib/maze/client/appium.rb +23 -0
  51. data/lib/maze/client/bb_api_client.rb +102 -0
  52. data/lib/maze/client/bb_client_utils.rb +181 -0
  53. data/lib/maze/client/bs_client_utils.rb +168 -0
  54. data/lib/maze/client/selenium/base_client.rb +15 -0
  55. data/lib/maze/client/selenium/bb_browsers.yml +188 -0
  56. data/lib/maze/client/selenium/bb_client.rb +38 -0
  57. data/lib/maze/client/selenium/bs_browsers.yml +257 -0
  58. data/lib/maze/client/selenium/bs_client.rb +89 -0
  59. data/lib/maze/client/selenium/local_client.rb +16 -0
  60. data/lib/maze/client/selenium.rb +16 -0
  61. data/lib/maze/compare.rb +161 -0
  62. data/lib/maze/configuration.rb +182 -0
  63. data/lib/maze/docker.rb +147 -0
  64. data/lib/maze/document_server.rb +46 -0
  65. data/lib/maze/driver/appium.rb +198 -0
  66. data/lib/maze/driver/browser.rb +124 -0
  67. data/lib/maze/errors.rb +52 -0
  68. data/lib/maze/generator.rb +55 -0
  69. data/lib/maze/helper.rb +122 -0
  70. data/lib/maze/hooks/appium_hooks.rb +55 -0
  71. data/lib/maze/hooks/browser_hooks.rb +15 -0
  72. data/lib/maze/hooks/command_hooks.rb +9 -0
  73. data/lib/maze/hooks/error_code_hook.rb +49 -0
  74. data/lib/maze/hooks/hooks.rb +61 -0
  75. data/lib/maze/http_request.rb +21 -0
  76. data/lib/maze/interactive_cli.rb +173 -0
  77. data/lib/maze/logger.rb +86 -0
  78. data/lib/maze/macos_utils.rb +14 -0
  79. data/lib/maze/maze_output.rb +88 -0
  80. data/lib/maze/network.rb +49 -0
  81. data/lib/maze/option/parser.rb +240 -0
  82. data/lib/maze/option/processor.rb +130 -0
  83. data/lib/maze/option/validator.rb +155 -0
  84. data/lib/maze/option.rb +62 -0
  85. data/lib/maze/plugins/bugsnag_reporting_plugin.rb +49 -0
  86. data/lib/maze/plugins/cucumber_report_plugin.rb +101 -0
  87. data/lib/maze/plugins/error_code_plugin.rb +21 -0
  88. data/lib/maze/plugins/global_retry_plugin.rb +38 -0
  89. data/lib/maze/proxy.rb +114 -0
  90. data/lib/maze/request_list.rb +87 -0
  91. data/lib/maze/request_repeater.rb +49 -0
  92. data/lib/maze/retry_handler.rb +67 -0
  93. data/lib/maze/runner.rb +149 -0
  94. data/lib/maze/schemas/OtelTraceSchema.json +390 -0
  95. data/lib/maze/schemas/trace_schema.rb +7 -0
  96. data/lib/maze/schemas/trace_validator.rb +98 -0
  97. data/lib/maze/server.rb +251 -0
  98. data/lib/maze/servlets/base_servlet.rb +27 -0
  99. data/lib/maze/servlets/command_servlet.rb +47 -0
  100. data/lib/maze/servlets/log_servlet.rb +64 -0
  101. data/lib/maze/servlets/reflective_servlet.rb +70 -0
  102. data/lib/maze/servlets/servlet.rb +199 -0
  103. data/lib/maze/servlets/temp.rb +0 -0
  104. data/lib/maze/servlets/trace_servlet.rb +13 -0
  105. data/lib/maze/store.rb +15 -0
  106. data/lib/maze/terminating_server.rb +129 -0
  107. data/lib/maze/timers.rb +51 -0
  108. data/lib/maze/wait.rb +35 -0
  109. data/lib/maze.rb +27 -0
  110. data/lib/utils/deep_merge.rb +17 -0
  111. data/lib/utils/selenium_money_patch.rb +17 -0
  112. metadata +451 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'securerandom'
5
+ require 'webrick'
6
+ require_relative './logger'
7
+ require_relative './request_list'
8
+
9
+ module Maze
10
+ # Receives and stores requests through a WEBrick HTTPServer
11
+ class Server
12
+ ALLOWED_HTTP_VERBS = %w[OPTIONS GET POST PUT DELETE HEAD TRACE PATCH CONNECT]
13
+ DEFAULT_RESPONSE_DELAY = 0
14
+ DEFAULT_SAMPLING_PROBABILITY = 1
15
+ DEFAULT_STATUS_CODE = 200
16
+
17
+ class << self
18
+ # Sets the response delay generator.
19
+ #
20
+ # @param generator [Maze::Generator] The new generator
21
+ def set_response_delay_generator(generator)
22
+ @response_delay_generator&.close
23
+ @response_delay_generator = generator
24
+ end
25
+
26
+ # Sets the sampling probability generator.
27
+ #
28
+ # @param generator [Maze::Generator] The new generator
29
+ def set_sampling_probability_generator(generator)
30
+ @sampling_probability_generator&.close
31
+ @sampling_probability_generator = generator
32
+ end
33
+
34
+ # Sets the status code generator for the HTTP verb given. If no verb is given then the
35
+ # generator will be shared across all allowable HTTP verbs.
36
+ #
37
+ # @param generator [Maze::Generator] The new generator
38
+ # @param verb [String] HTTP verb
39
+ def set_status_code_generator(generator, verb = nil)
40
+ @status_code_generators ||= {}
41
+ Array(verb || ALLOWED_HTTP_VERBS).each do |verb|
42
+ old = @status_code_generators[verb]
43
+ @status_code_generators[verb] = generator
44
+
45
+ # Close the old generator unless it's still being used by another verb
46
+ old&.close unless @status_code_generators.value?(old)
47
+ end
48
+ end
49
+
50
+ # The intended HTTP status code on a successful request
51
+ #
52
+ # @param verb [String] HTTP verb for which the status code is wanted
53
+ #
54
+ # @return [Integer] The HTTP status code for the verb given
55
+ def status_code(verb)
56
+ if @status_code_generators[verb].nil? || @status_code_generators[verb].closed?
57
+ DEFAULT_STATUS_CODE
58
+ else
59
+ @status_code_generators[verb].next
60
+ end
61
+ end
62
+
63
+ def sampling_probability
64
+ @sampling_probability_generator.next
65
+ end
66
+
67
+ def response_delay_ms
68
+ @response_delay_generator.next
69
+ end
70
+
71
+ # Provides dynamic access to request lists by name
72
+ #
73
+ # @param type [String, Symbol] Request type
74
+ # @return Request list for the type given
75
+ def list_for(type)
76
+ type = type.to_s
77
+ case type
78
+ when 'error', 'errors'
79
+ errors
80
+ when 'session', 'sessions'
81
+ sessions
82
+ when 'build', 'builds'
83
+ builds
84
+ when 'log', 'logs'
85
+ logs
86
+ when 'trace', 'traces'
87
+ traces
88
+ when 'upload', 'uploads'
89
+ uploads
90
+ when 'sourcemap', 'sourcemaps'
91
+ sourcemaps
92
+ when 'invalid', 'invalid requests'
93
+ invalid_requests
94
+ else
95
+ raise "Invalid request type '#{type}'"
96
+ end
97
+ end
98
+
99
+ # A list of error requests received
100
+ #
101
+ # @return [RequestList] Received error requests
102
+ def errors
103
+ @errors ||= RequestList.new
104
+ end
105
+
106
+ # A list of session requests received
107
+ #
108
+ # @return [RequestList] Received error requests
109
+ def sessions
110
+ @sessions ||= RequestList.new
111
+ end
112
+
113
+ # A list of trace requests received
114
+ #
115
+ # @return [RequestList] Received error requests
116
+ def traces
117
+ @traces ||= RequestList.new
118
+ end
119
+
120
+ # A list of build requests received
121
+ #
122
+ # @return [RequestList] Received build requests
123
+ def builds
124
+ @builds ||= RequestList.new
125
+ end
126
+
127
+ # A list of upload requests received
128
+ #
129
+ # @return [RequestList] Received upload requests
130
+ def uploads
131
+ @uploads ||= RequestList.new
132
+ end
133
+
134
+ # A list of sourcemap requests received
135
+ #
136
+ # @return [RequestList] Received sourcemap requests
137
+ def sourcemaps
138
+ @sourcemaps ||= RequestList.new
139
+ end
140
+
141
+ # A list of log requests received
142
+ #
143
+ # @return [RequestList] Received log requests
144
+ def logs
145
+ @logs ||= RequestList.new
146
+ end
147
+
148
+ # A list of commands for a test fixture to perform. Strictly speaking these are responses to HTTP
149
+ # requests, but the list behavior is all we need.
150
+ #
151
+ # @return [RequestList] Commands to be performed
152
+ def commands
153
+ @commands ||= RequestList.new
154
+ end
155
+
156
+ # Whether the server thread is running
157
+ # An array of any invalid requests received.
158
+ # Each request is hash consisting of:
159
+ # request: The original HTTPRequest object
160
+ # reason: Reason for being considered invalid. Examples include invalid JSON and missing/invalid digest.
161
+ # @return [RequestList] An array of received requests
162
+ def invalid_requests
163
+ @invalid_requests ||= RequestList.new
164
+ end
165
+
166
+ # Whether the server thread is running
167
+ #
168
+ # @return [Boolean] If the server is running
169
+ def running?
170
+ @thread&.alive?
171
+ end
172
+
173
+ # Starts the WEBrick server in a separate thread
174
+ def start
175
+ attempts = 0
176
+ loop do
177
+
178
+ @thread = Thread.new do
179
+ options = {
180
+ Port: Maze.config.port,
181
+ Logger: $logger,
182
+ AccessLog: []
183
+ }
184
+ options[:BindAddress] = Maze.config.bind_address unless Maze.config.bind_address.nil?
185
+ server = WEBrick::HTTPServer.new(options)
186
+
187
+ # Mount a block to respond to all requests with status:200
188
+ server.mount_proc '/' do |_request, response|
189
+ $logger.debug 'Received request on server root, responding with 200'
190
+ response.header['Access-Control-Allow-Origin'] = '*'
191
+ response.body = 'Maze runner received request'
192
+ response.status = 200
193
+ end
194
+
195
+ # When adding more endpoints, be sure to update the 'I should receive no requests' step
196
+ server.mount '/notify', Servlets::Servlet, :errors
197
+ server.mount '/sessions', Servlets::Servlet, :sessions
198
+ server.mount '/builds', Servlets::Servlet, :builds
199
+ server.mount '/uploads', Servlets::Servlet, :uploads
200
+ server.mount '/sourcemap', Servlets::Servlet, :sourcemaps
201
+ server.mount '/traces', Servlets::TraceServlet, :traces, Maze::Schemas::TRACE_SCHEMA
202
+ server.mount '/react-native-source-map', Servlets::Servlet, :sourcemaps
203
+ server.mount '/command', Servlets::CommandServlet
204
+ server.mount '/logs', Servlets::LogServlet
205
+ server.mount '/reflect', Servlets::ReflectiveServlet
206
+ server.start
207
+ rescue StandardError => e
208
+ $logger.warn "Failed to start mock server: #{e.message}"
209
+ ensure
210
+ server&.shutdown
211
+ end
212
+
213
+ # Need a short sleep here as a dying thread is still alive momentarily
214
+ sleep 1
215
+ break if running?
216
+
217
+ # Bail out after 3 attempts
218
+ attempts += 1
219
+ raise 'Too many failed attempts to start mock server' if attempts == 3
220
+
221
+ # Failed to start - sleep before retrying
222
+ $logger.info 'Retrying in 5 seconds'
223
+ sleep 5
224
+ end
225
+ end
226
+
227
+ # Stops the WEBrick server thread if it's running
228
+ def stop
229
+ @thread&.kill if @thread&.alive?
230
+ @thread = nil
231
+ end
232
+
233
+ def reset!
234
+ # Reset generators
235
+ set_response_delay_generator(Maze::Generator.new [DEFAULT_RESPONSE_DELAY].cycle)
236
+ set_status_code_generator(Maze::Generator.new [DEFAULT_STATUS_CODE].cycle)
237
+ set_sampling_probability_generator(Maze::Generator.new [DEFAULT_SAMPLING_PROBABILITY].cycle)
238
+
239
+ # Clear request lists
240
+ errors.clear
241
+ sessions.clear
242
+ builds.clear
243
+ uploads.clear
244
+ sourcemaps.clear
245
+ traces.clear
246
+ logs.clear
247
+ invalid_requests.clear
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+
6
+ # Base servlet to avoid duplication of common code
7
+ class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
8
+ # Logs and returns a set of valid headers for this servlet.
9
+ #
10
+ # @param request [HTTPRequest] The incoming GET request
11
+ # @param response [HTTPResponse] The response to return
12
+ def do_OPTIONS(request, response)
13
+ response.header['Access-Control-Allow-Origin'] = '*'
14
+ response.header['Access-Control-Allow-Headers'] = %w[
15
+ Accept
16
+ Bugsnag-Api-Key
17
+ Bugsnag-Integrity
18
+ Bugsnag-Payload-Version
19
+ Bugsnag-Sent-At
20
+ Bugsnag-Span-Sampling
21
+ Content-Type
22
+ Origin
23
+ ].join(',')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Maze
6
+ module Servlets
7
+
8
+ # Allows clients to queue up "commands", in the form of Ruby hashes, using Maze::Server.commands.add. GET
9
+ # requests made to the /command endpoint will then respond with each queued command in turn.
10
+ class CommandServlet < BaseServlet
11
+ # Serves the next command, if these is one.
12
+ #
13
+ # @param _request [HTTPRequest] The incoming GET request
14
+ # @param response [HTTPResponse] The response to return
15
+ def do_GET(_request, response)
16
+ response.header['Access-Control-Allow-Origin'] = '*'
17
+
18
+ commands = Maze::Server.commands
19
+
20
+ if commands.size_remaining == 0
21
+ response.body = '{"action": "noop", "message": "No commands queued"}'
22
+ response.status = 200
23
+ else
24
+ command = commands.current
25
+ command_json = JSON.pretty_generate(command)
26
+ command[:uuid] = Maze.run_uuid
27
+ response.body = command_json
28
+ response.status = 200
29
+ commands.next
30
+ end
31
+ end
32
+
33
+ # Logs and returns a set of valid headers for this servlet.
34
+ #
35
+ # @param request [HTTPRequest] The incoming GET request
36
+ # @param response [HTTPResponse] The response to return
37
+ def do_OPTIONS(request, response)
38
+ super
39
+
40
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
41
+ response.status = Server.status_code('OPTIONS')
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+
6
+ # Receives log requests sent from the test fixture
7
+ class LogServlet < BaseServlet
8
+ # Constructor
9
+ #
10
+ # @param server [HTTPServer] WEBrick HTTPServer
11
+ def initialize(server)
12
+ super server
13
+ @requests = Server.logs
14
+ end
15
+
16
+ # Logs and parses an incoming POST request.
17
+ # Parses `multipart/form-data` and `application/json` content-types.
18
+ # Parsed requests are added to the requests list.
19
+ #
20
+ # @param request [HTTPRequest] The incoming GET request
21
+ # @param response [HTTPResponse] The response to return
22
+ def do_POST(request, response)
23
+ hash = {
24
+ body: JSON.parse(request.body),
25
+ request: request
26
+ }
27
+ @requests.add(hash)
28
+
29
+ response.header['Access-Control-Allow-Origin'] = '*'
30
+ response.status = Server.status_code('POST')
31
+ rescue JSON::ParserError => e
32
+ msg = "Unable to parse request as JSON: #{e.message}"
33
+ $logger.error msg
34
+ Server.invalid_requests.add({
35
+ reason: msg,
36
+ request: request,
37
+ body: request.body
38
+ })
39
+ rescue StandardError => e
40
+ $logger.error "Invalid request: #{e.message}"
41
+ Server.invalid_requests.add({
42
+ invalid: true,
43
+ reason: e.message,
44
+ request: {
45
+ request_uri: request.request_uri,
46
+ header: request.header.to_h,
47
+ body: request.inspect
48
+ }
49
+ })
50
+ end
51
+
52
+ # Logs and returns a set of valid headers for this servlet.
53
+ #
54
+ # @param request [HTTPRequest] The incoming GET request
55
+ # @param response [HTTPResponse] The response to return
56
+ def do_OPTIONS(request, response)
57
+ super
58
+
59
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
60
+ response.status = Server.status_code('OPTIONS')
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ require 'rack'
3
+
4
+ module Maze
5
+ module Servlets
6
+ # Receives HTTP requests and responds according to the parameters given, which are:
7
+ # - delay_ms - milliseconds to wait before responding
8
+ # - status - HTTP response code
9
+ # For GET requests these are expected to passed as GET parameters,
10
+ # for POST requests they are expected to be given as JSON fields.
11
+ class ReflectiveServlet < BaseServlet
12
+
13
+ # Accepts a GET request to provide a reflective response to.
14
+ #
15
+ # @param request [HTTPRequest] The incoming GET request
16
+ # @param response [HTTPResponse] The response to return
17
+ def do_GET(request, response)
18
+ delay_ms = request.query['delay_ms']
19
+ status = request.query['status']
20
+ reflect response, delay_ms, status
21
+ end
22
+
23
+ # Accepts a POST request to provide a reflective response to.
24
+ #
25
+ # @param request [HTTPRequest] The incoming GET request
26
+ # @param response [HTTPResponse] The response to return
27
+ def do_POST(request, response)
28
+
29
+ content_type = request['Content-Type']
30
+
31
+ # For JSON, pull the instructions from the body. Otherwise, take them from the query string.
32
+ if content_type == 'application/json'
33
+ body = JSON.parse(request.body)
34
+ delay_ms = body['delay_ms']
35
+ status = body['status']
36
+ else
37
+ query = Rack::Utils.parse_nested_query(request.query_string)
38
+ delay_ms = query['delay_ms']
39
+ status = query['status']
40
+ end
41
+
42
+ reflect response, delay_ms, status
43
+ rescue JSON::ParserError => e
44
+ msg = "Unable to parse request as JSON: #{e.message}"
45
+ $logger.error msg
46
+ response.status = 418
47
+ rescue StandardError => e
48
+ $logger.error "Invalid request: #{e.message}"
49
+ response.status = 500
50
+ end
51
+
52
+ def reflect(response, delay_ms, status)
53
+ sleep delay_ms.to_i / 1000 unless delay_ms.nil?
54
+ response.status = status || 200
55
+ response.header['Access-Control-Allow-Origin'] = '*'
56
+ response.body = "Returned status #{status} after waiting #{delay_ms} ms"
57
+ end
58
+
59
+ # Logs and returns a set of valid headers for this servlet.
60
+ #
61
+ # @param request [HTTPRequest] The incoming GET request
62
+ # @param response [HTTPResponse] The response to return
63
+ def do_OPTIONS(request, response)
64
+ super
65
+
66
+ response.header['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+ require 'stringio'
5
+ require 'json_schemer'
6
+ require 'delegate'
7
+
8
+ module Maze
9
+ class HttpRequest < SimpleDelegator
10
+ def body
11
+ @body ||= decode_body
12
+ end
13
+
14
+ private
15
+
16
+ def decode_body
17
+ delegate = __getobj__
18
+ if %r{^gzip$}.match(delegate['Content-Encoding'])
19
+ gz_element = Zlib::GzipReader.new(StringIO.new(delegate.body))
20
+ gz_element.read
21
+ else
22
+ delegate.body
23
+ end
24
+ end
25
+ end
26
+
27
+ module Servlets
28
+
29
+ # Receives and parses the requests and payloads sent from the test fixture
30
+ class Servlet < BaseServlet
31
+ prepend RequestRepeater
32
+
33
+ # Constructor
34
+ #
35
+ # @param server [HTTPServer] WEBrick HTTPServer
36
+ # @param request_type [Symbol] Request type that the servlet will receive
37
+ # @param schema [Dictionary] A `json-schema` describing the payload for POST requests
38
+ def initialize(server, request_type, schema=nil)
39
+ super server
40
+ @request_type = request_type
41
+ @requests = Server.list_for request_type
42
+ @schema = JSONSchemer.schema(schema) unless schema.nil?
43
+ end
44
+
45
+ # Logs an incoming GET WEBrick request.
46
+ #
47
+ # @param request [HTTPRequest] The incoming GET request
48
+ # @param _response [HTTPResponse] The response to return
49
+ def do_GET(request, _response)
50
+ log_request(request)
51
+ end
52
+
53
+ # Logs and parses an incoming POST request.
54
+ # Parses `multipart/form-data` and `application/json` content-types.
55
+ # Parsed requests are added to the requests list.
56
+ #
57
+ # @param request [HTTPRequest] The incoming GET request
58
+ # @param response [HTTPResponse] The response to return
59
+ def do_POST(request, response)
60
+ # Turn the WEBrick HttpRequest into our internal HttpRequest delegate
61
+ request = HttpRequest.new(request)
62
+
63
+ content_type = request['Content-Type']
64
+ if %r{^multipart/form-data; boundary=([^;]+)}.match(content_type)
65
+ boundary = WEBrick::HTTPUtils::dequote($1)
66
+ body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
67
+ hash = {
68
+ body: body,
69
+ request: request
70
+ }
71
+ else
72
+ # "content-type" is assumed to be JSON (which mimics the behaviour of
73
+ # the actual API). This supports browsers that can't set this header for
74
+ # cross-domain requests (IE8/9)
75
+ digests = check_digest request
76
+ hash = {
77
+ body: JSON.parse(request.body),
78
+ request: request,
79
+ digests: digests
80
+ }
81
+ end
82
+ if @schema
83
+ schema_errors = @schema.validate(hash[:body])
84
+ hash[:schema_errors] = schema_errors.to_a
85
+ end
86
+ @requests.add(hash)
87
+
88
+ # For the response, delaying if configured to do so
89
+ response_delay_ms = Server.response_delay_ms
90
+ if response_delay_ms.positive?
91
+ $logger.info "Waiting #{response_delay_ms} milliseconds before responding"
92
+ sleep response_delay_ms / 1000.0
93
+ end
94
+ set_response_header response.header
95
+ response.status = Server.status_code('POST')
96
+ rescue JSON::ParserError => e
97
+ msg = "Unable to parse request as JSON: #{e.message}"
98
+ if Maze.config.captured_invalid_requests.include? @request_type
99
+ $logger.error msg
100
+ Server.invalid_requests.add({
101
+ reason: msg,
102
+ request: request,
103
+ body: request.body
104
+ })
105
+ else
106
+ $logger.warn msg
107
+ end
108
+ rescue StandardError => e
109
+ if Maze.config.captured_invalid_requests.include? @request_type
110
+ $logger.error "Invalid request: #{e.message}"
111
+ Server.invalid_requests.add({
112
+ invalid: true,
113
+ reason: e.message,
114
+ request: {
115
+ request_uri: request.request_uri,
116
+ header: request.header,
117
+ body: request.inspect
118
+ }
119
+ })
120
+ else
121
+ $logger.warn "Invalid request: #{e.message}"
122
+ end
123
+ end
124
+
125
+ def set_response_header(header)
126
+ header['Access-Control-Allow-Origin'] = '*'
127
+ end
128
+
129
+ # Logs and returns a set of valid headers for this servlet.
130
+ #
131
+ # @param request [HTTPRequest] The incoming GET request
132
+ # @param response [HTTPResponse] The response to return
133
+ def do_OPTIONS(request, response)
134
+ super
135
+
136
+ response.header['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
137
+ response.status = Server.status_code('OPTIONS')
138
+ end
139
+
140
+ private
141
+
142
+ def log_request(request)
143
+ $logger.debug "#{request.request_method} request received"
144
+ $logger.debug "URI: #{request.unparsed_uri}"
145
+ $logger.debug "HEADERS: #{request.raw_header}"
146
+ return if request.body.nil?
147
+
148
+ case request['Content-Type']
149
+ when nil
150
+ nil
151
+ when %r{^multipart/form-data; boundary=([^;]+)}
152
+ boundary = WEBrick::HTTPUtils.dequote(Regexp.last_match(1))
153
+ body = WEBrick::HTTPUtils.parse_form_data(request.body, boundary)
154
+ $logger.debug 'BODY:'
155
+ LogUtil.log_hash(Logger::Severity::DEBUG, body)
156
+ when %r{^application/json$}
157
+ $logger.debug "BODY: #{JSON.pretty_generate(JSON.parse(request.body))}"
158
+ else
159
+ $logger.debug "BODY: #{request.body}"
160
+ end
161
+ end
162
+
163
+ # Checks the Bugsnag-Integrity header, if present, against the request and based on configuration.
164
+ # If the header is present, if the digest must be correct. However, the header need only be present
165
+ # if configuration says so.
166
+ def check_digest(request)
167
+ header = request['Bugsnag-Integrity']
168
+ if header.nil? && Maze.config.enforce_bugsnag_integrity
169
+ raise 'Bugsnag-Integrity header must be present according to Maze.config.enforce_bugsnag_integrity'
170
+ end
171
+ return if header.nil?
172
+
173
+ # Header must have type and digest
174
+ parts = header.split ' '
175
+ raise "Invalid Bugsnag-Integrity header: #{header}" unless parts.size == 2
176
+
177
+ # Both digest types are stored whatever
178
+ sha1 = Digest::SHA1.hexdigest(request.body)
179
+ simple = request.body.bytesize
180
+ $logger.debug "DIGESTS computed: sha1=#{sha1} simple=#{simple}"
181
+
182
+ # Check digests match
183
+ case parts[0]
184
+ when 'sha1'
185
+ raise "Given sha1 #{parts[1]} does not match the computed #{sha1}" unless parts[1] == sha1
186
+ when 'simple'
187
+ raise "Given simple digest #{parts[1].inspect} does not match the computed #{simple.inspect}" unless parts[1].to_i == simple
188
+ else
189
+ raise "Invalid Bugsnag-Integrity digest type: #{parts[0]}"
190
+ end
191
+
192
+ {
193
+ sha1: sha1,
194
+ simple: simple
195
+ }
196
+ end
197
+ end
198
+ end
199
+ end
File without changes
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Servlets
5
+ class TraceServlet < Servlet
6
+ def set_response_header(header)
7
+ super
8
+ value = Maze::Server.sampling_probability
9
+ header['Bugsnag-Sampling-Probability'] = value unless value == 'null'
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/maze/store.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ # Provides a hash to store values for cross-request comparisons
5
+ class Store
6
+ class << self
7
+ # Returns the hash of stored values for the current scenario
8
+ #
9
+ # @return [Hash] The stored value hash
10
+ def values
11
+ @values ||= {}
12
+ end
13
+ end
14
+ end
15
+ end