This repository has been archived by the owner on Aug 7, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathapp.rb
388 lines (322 loc) · 9.36 KB
/
app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
require 'sinatra'
require 'slim'
require 'slim/include'
require 'redis'
require 'json'
require 'sinatra/flash'
require 'securerandom'
require 'rest_client'
class String
def titlecase
tr('_', ' ').
gsub(/\s+/, ' ').
gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
end
end
EXCLUDED_HEADERS = [
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_X_REQUEST_START',
'HTTP_X_VARNISH'
]
configure :development do
redis_url = (ENV["BOXEN_REDIS_URL"] || "redis://localhost:6379").chomp('/')
uri = URI.parse(redis_url)
REDIS = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
end
configure :production do
uri = URI.parse(ENV["REDISTOGO_URL"])
REDIS = Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
end
configure do
enable :sessions
disable :protection
RESERVED_CODES = %w{/runscope/export /runscope/oauth /runscope/logout}
RUNSCOPE_ID = ENV["RESPONDTOIT_RUNSCOPE_ID"]
RUNSCOPE_SECRET = ENV["RESPONDTOIT_RUNSCOPE_SECRET"]
end
helpers do
def supports_runscope?
RUNSCOPE_ID && RUNSCOPE_SECRET
end
def runscope_authenticated?
!session[:runscope_access_token].nil?
end
def requires_runscope
halt(404, "Runscope not supported") if !supports_runscope?
end
def requires_authenticated_runscope
requires_runscope
halt(404, "Not authenticated with Runscope") if !runscope_authenticated?
end
def runscope_buckets
session[:runscope_buckets] ||= fill_runscope_buckets
end
def fill_runscope_buckets
return nil if !session[:runscope_access_token]
response = RestClient.get "https://api.runscope.com/buckets", authorization: "Bearer #{session[:runscope_access_token]}"
if response.code == 200
result = JSON.parse(response)
session[:runscope_buckets] = result["data"].map { |e| { "default" => e["default"], "key" => e["key"], "name" => e["name"] } }
end
session[:runscope_buckets]
end
def default_runscope_bucket_key
default_bucket = runscope_buckets.find { |b| b["default"] }
default_bucket["key"] if default_bucket
end
def runscope_state
session[:runscope_state] ||= SecureRandom.uuid
end
def block_route_if_restricted
halt(404, "Sorry, this is reserved") if RESERVED_CODES.include? request.path_info
end
def code
params[:code] || params[:splat][0]
end
def config_key
"code:#{code}:config"
end
def requests_key
"code:#{code}:requests"
end
def analytics_key
"code:#{code}:hits"
end
def view?
request.query_string =~ /^view$/i
end
def destroy?
request.query_string =~ /^destroy$/i
end
def clear?
request.query_string =~ /^clear$/i
end
def json?
request.accept.first.to_s == "application/json" || params[:format] =~ /json/i
end
def json
content_type :json
config["json"]
end
def xml?
request.accept.first.to_s == "application/xml" || params[:format] =~ /xml/i
end
def xml
content_type :xml
config["xml"]
end
def known?
config[:known]
end
def unknown?
!known?
end
def active_if_unknown
'active' if unknown?
end
def active_if_known
'active' if known?
end
def config
@config ||= begin
data = REDIS.get config_key
if data.nil?
data = { :known => false }
else
data = JSON.parse(data)
data[:known] = true
end
data
end
end
def requests
@requests ||= begin
data = REDIS.lrange requests_key, 0, 9
data.map! { |req| JSON.parse(req) }
data.each { |req| req["time"] = Time.at(req["time"].to_f).utc }
end
end
def store_request
REDIS.multi do
REDIS.lpush requests_key, package_request # append the request to the end
REDIS.ltrim requests_key, 0, 9 # restrict to 10 items (but trim the first part of the list, keeping the last 10)
REDIS.expire requests_key, 172800 # delete all requests after 2 days: 2 * 24 * 60 * 60
end
end
def clear_requests
REDIS.DEL requests_key
end
def package_request
{
:id => SecureRandom.uuid,
:time => Time.now.utc.to_f,
:ip => request.ip,
:method => request.request_method.upcase,
:path => request.path,
:headers => package_headers,
:content_type => request.content_type,
:content_length => request.content_length,
:params => request.params,
:body => package_body
}.to_json
end
def package_headers
pretty = {}
# Select out all HTTP_* headers
allowed_headers = request.env.select { |k,v| k =~ /^HTTP_/ }
# Remove heroku headers
allowed_headers.delete_if { |k,v| k =~ /heroku/i || EXCLUDED_HEADERS.include?(k) }
# Add back CONTENT_LENGTH and CONTENT_TYPE
allowed_headers['CONTENT_LENGTH'] = request.content_length unless request.content_length.nil?
allowed_headers['CONTENT_TYPE'] = request.content_type unless request.content_type.nil?
allowed_headers.each do |k,v|
header = k.dup
header.gsub!(/^HTTP_/, '')
header = header.downcase.titlecase.tr(' ', '-')
pretty[header] = v
end
return pretty
end
def package_body
request.body.read if ((request.request_method == 'POST' || request.request_method == 'PUT') && !request.form_data?)
end
def update_analytics
request_method_field = request.request_method.downcase
daily_hit_field = "#{Date.today.to_s}:#{request.request_method.downcase}"
REDIS.multi do
REDIS.hincrby "hits", request_method_field, 1 # Global
REDIS.hincrby "hits", daily_hit_field, 1 # Global, daily
REDIS.hincrby analytics_key, request_method_field, 1 # Per-endpoint
REDIS.hincrby analytics_key, daily_hit_field, 1 # Per-endpoint, daily
end
end
end
get '/' do
slim :index
end
# Ensure that all of the following routes are included in RESERVED_CODES
get '/runscope/oauth' do
requires_runscope
halt(400) if !params[:code]
halt(400) if params[:state] != runscope_state
response = RestClient.post("https://www.runscope.com/signin/oauth/access_token",
{
client_id: RUNSCOPE_ID,
client_secret: RUNSCOPE_SECRET,
code: params[:code],
grant_type: 'authorization_code',
redirect_uri: url('/runscope/oauth')
})
if response.code == 200
result = JSON.parse(response)
session[:runscope_access_token] = result["access_token"]
end
redirect to(session[:last_view] || "/")
end
get '/runscope/logout' do
session[:runscope_access_token] = session[:runscope_buckets] = nil
redirect to(session[:last_view] || "/")
end
post '/runscope/export' do
requires_authenticated_runscope
halt(404, "Unknown request") if !code
bucket_key = params[:bucket_key] || default_runscope_bucket_key
halt(404, "Unable to determine destination bucket") if !bucket_key
req = requests.find { |r| params[:id] && r['id'] == params[:id] }
halt(404, "Unable to find the specified request") if req.nil?
data = {
request: {
method: req['method'],
url: url(req['path']),
headers: req['headers'] || {},
form: req['params'] || {},
body: req['body'] || '',
timestamp: req['time'].to_f
}
}.to_json
resp = RestClient.post "https://api.runscope.com/buckets/#{bucket_key}/messages",
data,
{ content_type: :json, authorization: "Bearer #{session[:runscope_access_token]}" }
if resp.code == 200
respData = JSON.parse(resp)
if respData['meta']['error_count'].to_i > 0
puts "Error exporting a request to runscope: #{resp}"
halt(500, "Error exporting the request to Runscope")
end
# Success! Build the link
"https://www.runscope.com/stream/#{bucket_key}"
end
end
['/*.:format?', '/*'].each do |path|
get path do
block_route_if_restricted
if clear?
clear_requests
return redirect to("/#{code}?view")
end
if view?
session[:last_view] = request.fullpath
return slim(:view)
end
return [404, "Um, guess again?"] if unknown?
store_request
update_analytics
request.session_options[:skip] = true
if json?
json
elsif xml?
xml
else
"Howdy"
end
end
post path do
block_route_if_restricted
if view?
msg = "The response was #{known? ? 'updated' : 'created'} successfully."
if !params[:json].to_s.empty? or !params[:xml].to_s.empty?
msg << " Check out the <a href='#{url("/#{code}.json")}'>JSON</a> or <a href='#{url("/#{code}.xml")}'>XML</a>"
end
config_hash = {:json => params[:json].to_s, :xml => params[:xml].to_s, :updated_at => Time.now.utc.to_i}
REDIS.set config_key, config_hash.to_json
flash[:notice] = msg
redirect to("/#{code}?view")
return
elsif destroy?
REDIS.multi do
REDIS.del config_key
REDIS.del requests_key
REDIS.del analytics_key
end
flash[:warning] = "The endpoint was destroyed."
redirect to("/#{code}?view")
return
end
return 404 if unknown?
store_request
update_analytics
request.session_options[:skip] = true
if json?
json
elsif xml?
xml
else
"Howdy"
end
end
put path do
block_route_if_restricted
return 404 if unknown?
store_request
update_analytics
request.session_options[:skip] = true
if json?
json
elsif xml?
xml
else
"Howdy"
end
end
end