sinatra-rack-3-commonlit 3.1.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 +7 -0
- data/.yardopts +5 -0
- data/AUTHORS.md +81 -0
- data/CHANGELOG.md +1742 -0
- data/CONTRIBUTING.md +100 -0
- data/Gemfile +68 -0
- data/LICENSE +26 -0
- data/MAINTENANCE.md +29 -0
- data/README.md +3016 -0
- data/Rakefile +216 -0
- data/SECURITY.md +35 -0
- data/VERSION +1 -0
- data/examples/chat.rb +75 -0
- data/examples/lifecycle_events.rb +20 -0
- data/examples/rainbows.conf +3 -0
- data/examples/rainbows.rb +22 -0
- data/examples/simple.rb +5 -0
- data/examples/stream.ru +27 -0
- data/lib/sinatra/base.rb +2111 -0
- data/lib/sinatra/images/404.png +0 -0
- data/lib/sinatra/images/500.png +0 -0
- data/lib/sinatra/indifferent_hash.rb +206 -0
- data/lib/sinatra/main.rb +56 -0
- data/lib/sinatra/show_exceptions.rb +364 -0
- data/lib/sinatra/version.rb +5 -0
- data/lib/sinatra.rb +5 -0
- data/sinatra.gemspec +56 -0
- metadata +202 -0
data/lib/sinatra/base.rb
ADDED
@@ -0,0 +1,2111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# external dependencies
|
4
|
+
require 'rack'
|
5
|
+
require 'rackup'
|
6
|
+
require 'tilt'
|
7
|
+
require 'rack/protection'
|
8
|
+
require 'mustermann'
|
9
|
+
require 'mustermann/sinatra'
|
10
|
+
require 'mustermann/regular'
|
11
|
+
|
12
|
+
# stdlib dependencies
|
13
|
+
require 'time'
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
# other files we need
|
17
|
+
require 'sinatra/indifferent_hash'
|
18
|
+
require 'sinatra/show_exceptions'
|
19
|
+
require 'sinatra/version'
|
20
|
+
|
21
|
+
module Sinatra
|
22
|
+
# The request object. See Rack::Request for more info:
|
23
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Request
|
24
|
+
class Request < Rack::Request
|
25
|
+
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze
|
26
|
+
HEADER_VALUE_WITH_PARAMS = %r{(?:(?:\w+|\*)/(?:\w+(?:\.|-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*}.freeze
|
27
|
+
|
28
|
+
# Returns an array of acceptable media types for the response
|
29
|
+
def accept
|
30
|
+
@env['sinatra.accept'] ||= if @env.include?('HTTP_ACCEPT') && (@env['HTTP_ACCEPT'].to_s != '')
|
31
|
+
@env['HTTP_ACCEPT']
|
32
|
+
.to_s
|
33
|
+
.scan(HEADER_VALUE_WITH_PARAMS)
|
34
|
+
.map! { |e| AcceptEntry.new(e) }
|
35
|
+
.sort
|
36
|
+
else
|
37
|
+
[AcceptEntry.new('*/*')]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def accept?(type)
|
42
|
+
preferred_type(type).to_s.include?(type)
|
43
|
+
end
|
44
|
+
|
45
|
+
def preferred_type(*types)
|
46
|
+
return accept.first if types.empty?
|
47
|
+
|
48
|
+
types.flatten!
|
49
|
+
return types.first if accept.empty?
|
50
|
+
|
51
|
+
accept.detect do |accept_header|
|
52
|
+
type = types.detect { |t| MimeTypeEntry.new(t).accepts?(accept_header) }
|
53
|
+
return type if type
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
alias secure? ssl?
|
58
|
+
|
59
|
+
def forwarded?
|
60
|
+
@env.include? 'HTTP_X_FORWARDED_HOST'
|
61
|
+
end
|
62
|
+
|
63
|
+
def safe?
|
64
|
+
get? || head? || options? || trace?
|
65
|
+
end
|
66
|
+
|
67
|
+
def idempotent?
|
68
|
+
safe? || put? || delete? || link? || unlink?
|
69
|
+
end
|
70
|
+
|
71
|
+
def link?
|
72
|
+
request_method == 'LINK'
|
73
|
+
end
|
74
|
+
|
75
|
+
def unlink?
|
76
|
+
request_method == 'UNLINK'
|
77
|
+
end
|
78
|
+
|
79
|
+
def params
|
80
|
+
super
|
81
|
+
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
|
82
|
+
raise BadRequest, "Invalid query parameters: #{Rack::Utils.escape_html(e.message)}"
|
83
|
+
rescue EOFError => e
|
84
|
+
raise BadRequest, "Invalid multipart/form-data: #{Rack::Utils.escape_html(e.message)}"
|
85
|
+
end
|
86
|
+
|
87
|
+
class AcceptEntry
|
88
|
+
attr_accessor :params
|
89
|
+
attr_reader :entry
|
90
|
+
|
91
|
+
def initialize(entry)
|
92
|
+
params = entry.scan(HEADER_PARAM).map! do |s|
|
93
|
+
key, value = s.strip.split('=', 2)
|
94
|
+
value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
|
95
|
+
[key, value]
|
96
|
+
end
|
97
|
+
|
98
|
+
@entry = entry
|
99
|
+
@type = entry[/[^;]+/].delete(' ')
|
100
|
+
@params = params.to_h
|
101
|
+
@q = @params.delete('q') { 1.0 }.to_f
|
102
|
+
end
|
103
|
+
|
104
|
+
def <=>(other)
|
105
|
+
other.priority <=> priority
|
106
|
+
end
|
107
|
+
|
108
|
+
def priority
|
109
|
+
# We sort in descending order; better matches should be higher.
|
110
|
+
[@q, -@type.count('*'), @params.size]
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_str
|
114
|
+
@type
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_s(full = false)
|
118
|
+
full ? entry : to_str
|
119
|
+
end
|
120
|
+
|
121
|
+
def respond_to?(*args)
|
122
|
+
super || to_str.respond_to?(*args)
|
123
|
+
end
|
124
|
+
|
125
|
+
def method_missing(*args, &block)
|
126
|
+
to_str.send(*args, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class MimeTypeEntry
|
131
|
+
attr_reader :params
|
132
|
+
|
133
|
+
def initialize(entry)
|
134
|
+
params = entry.scan(HEADER_PARAM).map! do |s|
|
135
|
+
key, value = s.strip.split('=', 2)
|
136
|
+
value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
|
137
|
+
[key, value]
|
138
|
+
end
|
139
|
+
|
140
|
+
@type = entry[/[^;]+/].delete(' ')
|
141
|
+
@params = params.to_h
|
142
|
+
end
|
143
|
+
|
144
|
+
def accepts?(entry)
|
145
|
+
File.fnmatch(entry, self) && matches_params?(entry.params)
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_str
|
149
|
+
@type
|
150
|
+
end
|
151
|
+
|
152
|
+
def matches_params?(params)
|
153
|
+
return true if @params.empty?
|
154
|
+
|
155
|
+
params.all? { |k, v| !@params.key?(k) || @params[k] == v }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# The response object. See Rack::Response and Rack::Response::Helpers for
|
161
|
+
# more info:
|
162
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Response
|
163
|
+
# https://rubydoc.info/github/rack/rack/main/Rack/Response/Helpers
|
164
|
+
class Response < Rack::Response
|
165
|
+
DROP_BODY_RESPONSES = [204, 304].freeze
|
166
|
+
|
167
|
+
def body=(value)
|
168
|
+
value = value.body while Rack::Response === value
|
169
|
+
@body = String === value ? [value.to_str] : value
|
170
|
+
end
|
171
|
+
|
172
|
+
def each
|
173
|
+
block_given? ? super : enum_for(:each)
|
174
|
+
end
|
175
|
+
|
176
|
+
def finish
|
177
|
+
result = body
|
178
|
+
|
179
|
+
if drop_content_info?
|
180
|
+
headers.delete 'content-length'
|
181
|
+
headers.delete 'content-type'
|
182
|
+
end
|
183
|
+
|
184
|
+
if drop_body?
|
185
|
+
close
|
186
|
+
result = []
|
187
|
+
end
|
188
|
+
|
189
|
+
if calculate_content_length?
|
190
|
+
# if some other code has already set content-length, don't muck with it
|
191
|
+
# currently, this would be the static file-handler
|
192
|
+
headers['content-length'] = body.map(&:bytesize).reduce(0, :+).to_s
|
193
|
+
end
|
194
|
+
|
195
|
+
[status, headers, result]
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def calculate_content_length?
|
201
|
+
headers['content-type'] && !headers['content-length'] && (Array === body)
|
202
|
+
end
|
203
|
+
|
204
|
+
def drop_content_info?
|
205
|
+
informational? || drop_body?
|
206
|
+
end
|
207
|
+
|
208
|
+
def drop_body?
|
209
|
+
DROP_BODY_RESPONSES.include?(status)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Some Rack handlers (Rainbows!) implement an extended body object protocol, however,
|
214
|
+
# some middleware (namely Rack::Lint) will break it by not mirroring the methods in question.
|
215
|
+
# This middleware will detect an extended body object and will make sure it reaches the
|
216
|
+
# handler directly. We do this here, so our middleware and middleware set up by the app will
|
217
|
+
# still be able to run.
|
218
|
+
class ExtendedRack < Struct.new(:app)
|
219
|
+
def call(env)
|
220
|
+
result = app.call(env)
|
221
|
+
callback = env['async.callback']
|
222
|
+
return result unless callback && async?(*result)
|
223
|
+
|
224
|
+
after_response { callback.call result }
|
225
|
+
setup_close(env, *result)
|
226
|
+
throw :async
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def setup_close(env, _status, _headers, body)
|
232
|
+
return unless body.respond_to?(:close) && env.include?('async.close')
|
233
|
+
|
234
|
+
env['async.close'].callback { body.close }
|
235
|
+
env['async.close'].errback { body.close }
|
236
|
+
end
|
237
|
+
|
238
|
+
def after_response(&block)
|
239
|
+
raise NotImplementedError, 'only supports EventMachine at the moment' unless defined? EventMachine
|
240
|
+
|
241
|
+
EventMachine.next_tick(&block)
|
242
|
+
end
|
243
|
+
|
244
|
+
def async?(status, _headers, body)
|
245
|
+
return true if status == -1
|
246
|
+
|
247
|
+
body.respond_to?(:callback) && body.respond_to?(:errback)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
|
252
|
+
# if another CommonLogger is already in the middleware chain.
|
253
|
+
class CommonLogger < Rack::CommonLogger
|
254
|
+
def call(env)
|
255
|
+
env['sinatra.commonlogger'] ? @app.call(env) : super
|
256
|
+
end
|
257
|
+
|
258
|
+
superclass.class_eval do
|
259
|
+
alias_method :call_without_check, :call unless method_defined? :call_without_check
|
260
|
+
def call(env)
|
261
|
+
env['sinatra.commonlogger'] = true
|
262
|
+
call_without_check(env)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
class Error < StandardError # :nodoc:
|
268
|
+
end
|
269
|
+
|
270
|
+
class BadRequest < Error # :nodoc:
|
271
|
+
def http_status; 400 end
|
272
|
+
end
|
273
|
+
|
274
|
+
class NotFound < Error # :nodoc:
|
275
|
+
def http_status; 404 end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Methods available to routes, before/after filters, and views.
|
279
|
+
module Helpers
|
280
|
+
# Set or retrieve the response status code.
|
281
|
+
def status(value = nil)
|
282
|
+
response.status = Rack::Utils.status_code(value) if value
|
283
|
+
response.status
|
284
|
+
end
|
285
|
+
|
286
|
+
# Set or retrieve the response body. When a block is given,
|
287
|
+
# evaluation is deferred until the body is read with #each.
|
288
|
+
def body(value = nil, &block)
|
289
|
+
if block_given?
|
290
|
+
def block.each; yield(call) end
|
291
|
+
response.body = block
|
292
|
+
elsif value
|
293
|
+
unless request.head? || value.is_a?(Rack::Files::Iterator) || value.is_a?(Stream)
|
294
|
+
headers.delete 'content-length'
|
295
|
+
end
|
296
|
+
response.body = value
|
297
|
+
else
|
298
|
+
response.body
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Halt processing and redirect to the URI provided.
|
303
|
+
def redirect(uri, *args)
|
304
|
+
# SERVER_PROTOCOL is required in Rack 3, fall back to HTTP_VERSION
|
305
|
+
# for servers not updated for Rack 3 (like Puma 5)
|
306
|
+
http_version = env['SERVER_PROTOCOL'] || env['HTTP_VERSION']
|
307
|
+
if (http_version == 'HTTP/1.1') && (env['REQUEST_METHOD'] != 'GET')
|
308
|
+
status 303
|
309
|
+
else
|
310
|
+
status 302
|
311
|
+
end
|
312
|
+
|
313
|
+
# According to RFC 2616 section 14.30, "the field value consists of a
|
314
|
+
# single absolute URI"
|
315
|
+
response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?)
|
316
|
+
halt(*args)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Generates the absolute URI for a given path in the app.
|
320
|
+
# Takes Rack routers and reverse proxies into account.
|
321
|
+
def uri(addr = nil, absolute = true, add_script_name = true)
|
322
|
+
return addr if addr.to_s =~ /\A[a-z][a-z0-9+.\-]*:/i
|
323
|
+
|
324
|
+
uri = [host = String.new]
|
325
|
+
if absolute
|
326
|
+
host << "http#{'s' if request.secure?}://"
|
327
|
+
host << if request.forwarded? || (request.port != (request.secure? ? 443 : 80))
|
328
|
+
request.host_with_port
|
329
|
+
else
|
330
|
+
request.host
|
331
|
+
end
|
332
|
+
end
|
333
|
+
uri << request.script_name.to_s if add_script_name
|
334
|
+
uri << (addr || request.path_info).to_s
|
335
|
+
File.join uri
|
336
|
+
end
|
337
|
+
|
338
|
+
alias url uri
|
339
|
+
alias to uri
|
340
|
+
|
341
|
+
# Halt processing and return the error status provided.
|
342
|
+
def error(code, body = nil)
|
343
|
+
if code.respond_to? :to_str
|
344
|
+
body = code.to_str
|
345
|
+
code = 500
|
346
|
+
end
|
347
|
+
response.body = body unless body.nil?
|
348
|
+
halt code
|
349
|
+
end
|
350
|
+
|
351
|
+
# Halt processing and return a 404 Not Found.
|
352
|
+
def not_found(body = nil)
|
353
|
+
error 404, body
|
354
|
+
end
|
355
|
+
|
356
|
+
# Set multiple response headers with Hash.
|
357
|
+
def headers(hash = nil)
|
358
|
+
response.headers.merge! hash if hash
|
359
|
+
response.headers
|
360
|
+
end
|
361
|
+
|
362
|
+
# Access the underlying Rack session.
|
363
|
+
def session
|
364
|
+
request.session
|
365
|
+
end
|
366
|
+
|
367
|
+
# Access shared logger object.
|
368
|
+
def logger
|
369
|
+
request.logger
|
370
|
+
end
|
371
|
+
|
372
|
+
# Look up a media type by file extension in Rack's mime registry.
|
373
|
+
def mime_type(type)
|
374
|
+
Base.mime_type(type)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Set the content-type of the response body given a media type or file
|
378
|
+
# extension.
|
379
|
+
def content_type(type = nil, params = {})
|
380
|
+
return response['content-type'] unless type
|
381
|
+
|
382
|
+
default = params.delete :default
|
383
|
+
mime_type = mime_type(type) || default
|
384
|
+
raise format('Unknown media type: %p', type) if mime_type.nil?
|
385
|
+
|
386
|
+
mime_type = mime_type.dup
|
387
|
+
unless params.include?(:charset) || settings.add_charset.all? { |p| !(p === mime_type) }
|
388
|
+
params[:charset] = params.delete('charset') || settings.default_encoding
|
389
|
+
end
|
390
|
+
params.delete :charset if mime_type.include? 'charset'
|
391
|
+
unless params.empty?
|
392
|
+
mime_type << (mime_type.include?(';') ? ', ' : ';')
|
393
|
+
mime_type << params.map do |key, val|
|
394
|
+
val = val.inspect if val =~ /[";,]/
|
395
|
+
"#{key}=#{val}"
|
396
|
+
end.join(', ')
|
397
|
+
end
|
398
|
+
response['content-type'] = mime_type
|
399
|
+
end
|
400
|
+
|
401
|
+
# https://html.spec.whatwg.org/#multipart-form-data
|
402
|
+
MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
|
403
|
+
'"' => '%22',
|
404
|
+
"\r" => '%0D',
|
405
|
+
"\n" => '%0A'
|
406
|
+
}.freeze
|
407
|
+
|
408
|
+
# Set the Content-Disposition to "attachment" with the specified filename,
|
409
|
+
# instructing the user agents to prompt to save.
|
410
|
+
def attachment(filename = nil, disposition = :attachment)
|
411
|
+
response['Content-Disposition'] = disposition.to_s.dup
|
412
|
+
return unless filename
|
413
|
+
|
414
|
+
params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE))
|
415
|
+
response['Content-Disposition'] << params
|
416
|
+
ext = File.extname(filename)
|
417
|
+
content_type(ext) unless response['content-type'] || ext.empty?
|
418
|
+
end
|
419
|
+
|
420
|
+
# Use the contents of the file at +path+ as the response body.
|
421
|
+
def send_file(path, opts = {})
|
422
|
+
if opts[:type] || !response['content-type']
|
423
|
+
content_type opts[:type] || File.extname(path), default: 'application/octet-stream'
|
424
|
+
end
|
425
|
+
|
426
|
+
disposition = opts[:disposition]
|
427
|
+
filename = opts[:filename]
|
428
|
+
disposition = :attachment if disposition.nil? && filename
|
429
|
+
filename = path if filename.nil?
|
430
|
+
attachment(filename, disposition) if disposition
|
431
|
+
|
432
|
+
last_modified opts[:last_modified] if opts[:last_modified]
|
433
|
+
|
434
|
+
file = Rack::Files.new(File.dirname(settings.app_file))
|
435
|
+
result = file.serving(request, path)
|
436
|
+
|
437
|
+
result[1].each { |k, v| headers[k] ||= v }
|
438
|
+
headers['content-length'] = result[1]['content-length']
|
439
|
+
opts[:status] &&= Integer(opts[:status])
|
440
|
+
halt (opts[:status] || result[0]), result[2]
|
441
|
+
rescue Errno::ENOENT
|
442
|
+
not_found
|
443
|
+
end
|
444
|
+
|
445
|
+
# Class of the response body in case you use #stream.
|
446
|
+
#
|
447
|
+
# Three things really matter: The front and back block (back being the
|
448
|
+
# block generating content, front the one sending it to the client) and
|
449
|
+
# the scheduler, integrating with whatever concurrency feature the Rack
|
450
|
+
# handler is using.
|
451
|
+
#
|
452
|
+
# Scheduler has to respond to defer and schedule.
|
453
|
+
class Stream
|
454
|
+
def self.schedule(*) yield end
|
455
|
+
def self.defer(*) yield end
|
456
|
+
|
457
|
+
def initialize(scheduler = self.class, keep_open = false, &back)
|
458
|
+
@back = back.to_proc
|
459
|
+
@scheduler = scheduler
|
460
|
+
@keep_open = keep_open
|
461
|
+
@callbacks = []
|
462
|
+
@closed = false
|
463
|
+
end
|
464
|
+
|
465
|
+
def close
|
466
|
+
return if closed?
|
467
|
+
|
468
|
+
@closed = true
|
469
|
+
@scheduler.schedule { @callbacks.each { |c| c.call } }
|
470
|
+
end
|
471
|
+
|
472
|
+
def each(&front)
|
473
|
+
@front = front
|
474
|
+
@scheduler.defer do
|
475
|
+
begin
|
476
|
+
@back.call(self)
|
477
|
+
rescue Exception => e
|
478
|
+
@scheduler.schedule { raise e }
|
479
|
+
ensure
|
480
|
+
close unless @keep_open
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def <<(data)
|
486
|
+
@scheduler.schedule { @front.call(data.to_s) }
|
487
|
+
self
|
488
|
+
end
|
489
|
+
|
490
|
+
def callback(&block)
|
491
|
+
return yield if closed?
|
492
|
+
|
493
|
+
@callbacks << block
|
494
|
+
end
|
495
|
+
|
496
|
+
alias errback callback
|
497
|
+
|
498
|
+
def closed?
|
499
|
+
@closed
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
# Allows to start sending data to the client even though later parts of
|
504
|
+
# the response body have not yet been generated.
|
505
|
+
#
|
506
|
+
# The close parameter specifies whether Stream#close should be called
|
507
|
+
# after the block has been executed. This is only relevant for evented
|
508
|
+
# servers like Rainbows.
|
509
|
+
def stream(keep_open = false)
|
510
|
+
scheduler = env['async.callback'] ? EventMachine : Stream
|
511
|
+
current = @params.dup
|
512
|
+
stream = if scheduler == Stream && keep_open
|
513
|
+
Stream.new(scheduler, false) do |out|
|
514
|
+
until out.closed?
|
515
|
+
with_params(current) { yield(out) }
|
516
|
+
end
|
517
|
+
end
|
518
|
+
else
|
519
|
+
Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
|
520
|
+
end
|
521
|
+
body stream
|
522
|
+
end
|
523
|
+
|
524
|
+
# Specify response freshness policy for HTTP caches (Cache-Control header).
|
525
|
+
# Any number of non-value directives (:public, :private, :no_cache,
|
526
|
+
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
|
527
|
+
# a Hash of value directives (:max_age, :s_maxage).
|
528
|
+
#
|
529
|
+
# cache_control :public, :must_revalidate, :max_age => 60
|
530
|
+
# => Cache-Control: public, must-revalidate, max-age=60
|
531
|
+
#
|
532
|
+
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
533
|
+
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
534
|
+
def cache_control(*values)
|
535
|
+
if values.last.is_a?(Hash)
|
536
|
+
hash = values.pop
|
537
|
+
hash.reject! { |_k, v| v == false }
|
538
|
+
hash.reject! { |k, v| values << k if v == true }
|
539
|
+
else
|
540
|
+
hash = {}
|
541
|
+
end
|
542
|
+
|
543
|
+
values.map! { |value| value.to_s.tr('_', '-') }
|
544
|
+
hash.each do |key, value|
|
545
|
+
key = key.to_s.tr('_', '-')
|
546
|
+
value = value.to_i if %w[max-age s-maxage].include? key
|
547
|
+
values << "#{key}=#{value}"
|
548
|
+
end
|
549
|
+
|
550
|
+
response['Cache-Control'] = values.join(', ') if values.any?
|
551
|
+
end
|
552
|
+
|
553
|
+
# Set the Expires header and Cache-Control/max-age directive. Amount
|
554
|
+
# can be an integer number of seconds in the future or a Time object
|
555
|
+
# indicating when the response should be considered "stale". The remaining
|
556
|
+
# "values" arguments are passed to the #cache_control helper:
|
557
|
+
#
|
558
|
+
# expires 500, :public, :must_revalidate
|
559
|
+
# => Cache-Control: public, must-revalidate, max-age=500
|
560
|
+
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
|
561
|
+
#
|
562
|
+
def expires(amount, *values)
|
563
|
+
values << {} unless values.last.is_a?(Hash)
|
564
|
+
|
565
|
+
if amount.is_a? Integer
|
566
|
+
time = Time.now + amount.to_i
|
567
|
+
max_age = amount
|
568
|
+
else
|
569
|
+
time = time_for amount
|
570
|
+
max_age = time - Time.now
|
571
|
+
end
|
572
|
+
|
573
|
+
values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 }
|
574
|
+
cache_control(*values)
|
575
|
+
|
576
|
+
response['Expires'] = time.httpdate
|
577
|
+
end
|
578
|
+
|
579
|
+
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
|
580
|
+
# and halt if conditional GET matches. The +time+ argument is a Time,
|
581
|
+
# DateTime, or other object that responds to +to_time+.
|
582
|
+
#
|
583
|
+
# When the current request includes an 'If-Modified-Since' header that is
|
584
|
+
# equal or later than the time specified, execution is immediately halted
|
585
|
+
# with a '304 Not Modified' response.
|
586
|
+
def last_modified(time)
|
587
|
+
return unless time
|
588
|
+
|
589
|
+
time = time_for time
|
590
|
+
response['Last-Modified'] = time.httpdate
|
591
|
+
return if env['HTTP_IF_NONE_MATCH']
|
592
|
+
|
593
|
+
if (status == 200) && env['HTTP_IF_MODIFIED_SINCE']
|
594
|
+
# compare based on seconds since epoch
|
595
|
+
since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
|
596
|
+
halt 304 if since >= time.to_i
|
597
|
+
end
|
598
|
+
|
599
|
+
if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE']
|
600
|
+
# compare based on seconds since epoch
|
601
|
+
since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
|
602
|
+
halt 412 if since < time.to_i
|
603
|
+
end
|
604
|
+
rescue ArgumentError
|
605
|
+
end
|
606
|
+
|
607
|
+
ETAG_KINDS = %i[strong weak].freeze
|
608
|
+
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
|
609
|
+
# GET matches. The +value+ argument is an identifier that uniquely
|
610
|
+
# identifies the current version of the resource. The +kind+ argument
|
611
|
+
# indicates whether the etag should be used as a :strong (default) or :weak
|
612
|
+
# cache validator.
|
613
|
+
#
|
614
|
+
# When the current request includes an 'If-None-Match' header with a
|
615
|
+
# matching etag, execution is immediately halted. If the request method is
|
616
|
+
# GET or HEAD, a '304 Not Modified' response is sent.
|
617
|
+
def etag(value, options = {})
|
618
|
+
# Before touching this code, please double check RFC 2616 14.24 and 14.26.
|
619
|
+
options = { kind: options } unless Hash === options
|
620
|
+
kind = options[:kind] || :strong
|
621
|
+
new_resource = options.fetch(:new_resource) { request.post? }
|
622
|
+
|
623
|
+
unless ETAG_KINDS.include?(kind)
|
624
|
+
raise ArgumentError, ':strong or :weak expected'
|
625
|
+
end
|
626
|
+
|
627
|
+
value = format('"%s"', value)
|
628
|
+
value = "W/#{value}" if kind == :weak
|
629
|
+
response['ETag'] = value
|
630
|
+
|
631
|
+
return unless success? || status == 304
|
632
|
+
|
633
|
+
if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource)
|
634
|
+
halt(request.safe? ? 304 : 412)
|
635
|
+
end
|
636
|
+
|
637
|
+
if env['HTTP_IF_MATCH']
|
638
|
+
return if etag_matches?(env['HTTP_IF_MATCH'], new_resource)
|
639
|
+
|
640
|
+
halt 412
|
641
|
+
end
|
642
|
+
|
643
|
+
nil
|
644
|
+
end
|
645
|
+
|
646
|
+
# Sugar for redirect (example: redirect back)
|
647
|
+
def back
|
648
|
+
request.referer
|
649
|
+
end
|
650
|
+
|
651
|
+
# whether or not the status is set to 1xx
|
652
|
+
def informational?
|
653
|
+
status.between? 100, 199
|
654
|
+
end
|
655
|
+
|
656
|
+
# whether or not the status is set to 2xx
|
657
|
+
def success?
|
658
|
+
status.between? 200, 299
|
659
|
+
end
|
660
|
+
|
661
|
+
# whether or not the status is set to 3xx
|
662
|
+
def redirect?
|
663
|
+
status.between? 300, 399
|
664
|
+
end
|
665
|
+
|
666
|
+
# whether or not the status is set to 4xx
|
667
|
+
def client_error?
|
668
|
+
status.between? 400, 499
|
669
|
+
end
|
670
|
+
|
671
|
+
# whether or not the status is set to 5xx
|
672
|
+
def server_error?
|
673
|
+
status.between? 500, 599
|
674
|
+
end
|
675
|
+
|
676
|
+
# whether or not the status is set to 404
|
677
|
+
def not_found?
|
678
|
+
status == 404
|
679
|
+
end
|
680
|
+
|
681
|
+
# whether or not the status is set to 400
|
682
|
+
def bad_request?
|
683
|
+
status == 400
|
684
|
+
end
|
685
|
+
|
686
|
+
# Generates a Time object from the given value.
|
687
|
+
# Used by #expires and #last_modified.
|
688
|
+
def time_for(value)
|
689
|
+
if value.is_a? Numeric
|
690
|
+
Time.at value
|
691
|
+
elsif value.respond_to? :to_s
|
692
|
+
Time.parse value.to_s
|
693
|
+
else
|
694
|
+
value.to_time
|
695
|
+
end
|
696
|
+
rescue ArgumentError => e
|
697
|
+
raise e
|
698
|
+
rescue Exception
|
699
|
+
raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
|
700
|
+
end
|
701
|
+
|
702
|
+
private
|
703
|
+
|
704
|
+
# Helper method checking if a ETag value list includes the current ETag.
|
705
|
+
def etag_matches?(list, new_resource = request.post?)
|
706
|
+
return !new_resource if list == '*'
|
707
|
+
|
708
|
+
list.to_s.split(/\s*,\s*/).include? response['ETag']
|
709
|
+
end
|
710
|
+
|
711
|
+
def with_params(temp_params)
|
712
|
+
original = @params
|
713
|
+
@params = temp_params
|
714
|
+
yield
|
715
|
+
ensure
|
716
|
+
@params = original if original
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
# Template rendering methods. Each method takes the name of a template
|
721
|
+
# to render as a Symbol and returns a String with the rendered output,
|
722
|
+
# as well as an optional hash with additional options.
|
723
|
+
#
|
724
|
+
# `template` is either the name or path of the template as symbol
|
725
|
+
# (Use `:'subdir/myview'` for views in subdirectories), or a string
|
726
|
+
# that will be rendered.
|
727
|
+
#
|
728
|
+
# Possible options are:
|
729
|
+
# :content_type The content type to use, same arguments as content_type.
|
730
|
+
# :layout If set to something falsy, no layout is rendered, otherwise
|
731
|
+
# the specified layout is used (Ignored for `sass`)
|
732
|
+
# :layout_engine Engine to use for rendering the layout.
|
733
|
+
# :locals A hash with local variables that should be available
|
734
|
+
# in the template
|
735
|
+
# :scope If set, template is evaluate with the binding of the given
|
736
|
+
# object rather than the application instance.
|
737
|
+
# :views Views directory to use.
|
738
|
+
module Templates
|
739
|
+
module ContentTyped
|
740
|
+
attr_accessor :content_type
|
741
|
+
end
|
742
|
+
|
743
|
+
def initialize
|
744
|
+
super
|
745
|
+
@default_layout = :layout
|
746
|
+
@preferred_extension = nil
|
747
|
+
end
|
748
|
+
|
749
|
+
def erb(template, options = {}, locals = {}, &block)
|
750
|
+
render(:erb, template, options, locals, &block)
|
751
|
+
end
|
752
|
+
|
753
|
+
def haml(template, options = {}, locals = {}, &block)
|
754
|
+
render(:haml, template, options, locals, &block)
|
755
|
+
end
|
756
|
+
|
757
|
+
def sass(template, options = {}, locals = {})
|
758
|
+
options[:default_content_type] = :css
|
759
|
+
options[:exclude_outvar] = true
|
760
|
+
options[:layout] = nil
|
761
|
+
render :sass, template, options, locals
|
762
|
+
end
|
763
|
+
|
764
|
+
def scss(template, options = {}, locals = {})
|
765
|
+
options[:default_content_type] = :css
|
766
|
+
options[:exclude_outvar] = true
|
767
|
+
options[:layout] = nil
|
768
|
+
render :scss, template, options, locals
|
769
|
+
end
|
770
|
+
|
771
|
+
def builder(template = nil, options = {}, locals = {}, &block)
|
772
|
+
options[:default_content_type] = :xml
|
773
|
+
render_ruby(:builder, template, options, locals, &block)
|
774
|
+
end
|
775
|
+
|
776
|
+
def liquid(template, options = {}, locals = {}, &block)
|
777
|
+
render(:liquid, template, options, locals, &block)
|
778
|
+
end
|
779
|
+
|
780
|
+
def markdown(template, options = {}, locals = {})
|
781
|
+
options[:exclude_outvar] = true
|
782
|
+
render :markdown, template, options, locals
|
783
|
+
end
|
784
|
+
|
785
|
+
def rdoc(template, options = {}, locals = {})
|
786
|
+
render :rdoc, template, options, locals
|
787
|
+
end
|
788
|
+
|
789
|
+
def asciidoc(template, options = {}, locals = {})
|
790
|
+
render :asciidoc, template, options, locals
|
791
|
+
end
|
792
|
+
|
793
|
+
def markaby(template = nil, options = {}, locals = {}, &block)
|
794
|
+
render_ruby(:mab, template, options, locals, &block)
|
795
|
+
end
|
796
|
+
|
797
|
+
def nokogiri(template = nil, options = {}, locals = {}, &block)
|
798
|
+
options[:default_content_type] = :xml
|
799
|
+
render_ruby(:nokogiri, template, options, locals, &block)
|
800
|
+
end
|
801
|
+
|
802
|
+
def slim(template, options = {}, locals = {}, &block)
|
803
|
+
render(:slim, template, options, locals, &block)
|
804
|
+
end
|
805
|
+
|
806
|
+
def yajl(template, options = {}, locals = {})
|
807
|
+
options[:default_content_type] = :json
|
808
|
+
render :yajl, template, options, locals
|
809
|
+
end
|
810
|
+
|
811
|
+
def rabl(template, options = {}, locals = {})
|
812
|
+
Rabl.register!
|
813
|
+
render :rabl, template, options, locals
|
814
|
+
end
|
815
|
+
|
816
|
+
# Calls the given block for every possible template file in views,
|
817
|
+
# named name.ext, where ext is registered on engine.
|
818
|
+
def find_template(views, name, engine)
|
819
|
+
yield ::File.join(views, "#{name}.#{@preferred_extension}")
|
820
|
+
|
821
|
+
Tilt.default_mapping.extensions_for(engine).each do |ext|
|
822
|
+
yield ::File.join(views, "#{name}.#{ext}") unless ext == @preferred_extension
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
private
|
827
|
+
|
828
|
+
# logic shared between builder and nokogiri
|
829
|
+
def render_ruby(engine, template, options = {}, locals = {}, &block)
|
830
|
+
if template.is_a?(Hash)
|
831
|
+
options = template
|
832
|
+
template = nil
|
833
|
+
end
|
834
|
+
template = proc { block } if template.nil?
|
835
|
+
render engine, template, options, locals
|
836
|
+
end
|
837
|
+
|
838
|
+
def render(engine, data, options = {}, locals = {}, &block)
|
839
|
+
# merge app-level options
|
840
|
+
engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
|
841
|
+
options.merge!(engine_options) { |_key, v1, _v2| v1 }
|
842
|
+
|
843
|
+
# extract generic options
|
844
|
+
locals = options.delete(:locals) || locals || {}
|
845
|
+
views = options.delete(:views) || settings.views || './views'
|
846
|
+
layout = options[:layout]
|
847
|
+
layout = false if layout.nil? && options.include?(:layout)
|
848
|
+
eat_errors = layout.nil?
|
849
|
+
layout = engine_options[:layout] if layout.nil? || (layout == true && engine_options[:layout] != false)
|
850
|
+
layout = @default_layout if layout.nil? || (layout == true)
|
851
|
+
layout_options = options.delete(:layout_options) || {}
|
852
|
+
content_type = options.delete(:default_content_type)
|
853
|
+
content_type = options.delete(:content_type) || content_type
|
854
|
+
layout_engine = options.delete(:layout_engine) || engine
|
855
|
+
scope = options.delete(:scope) || self
|
856
|
+
exclude_outvar = options.delete(:exclude_outvar)
|
857
|
+
options.delete(:layout)
|
858
|
+
|
859
|
+
# set some defaults
|
860
|
+
options[:outvar] ||= '@_out_buf' unless exclude_outvar
|
861
|
+
options[:default_encoding] ||= settings.default_encoding
|
862
|
+
|
863
|
+
# compile and render template
|
864
|
+
begin
|
865
|
+
layout_was = @default_layout
|
866
|
+
@default_layout = false
|
867
|
+
template = compile_template(engine, data, options, views)
|
868
|
+
output = template.render(scope, locals, &block)
|
869
|
+
ensure
|
870
|
+
@default_layout = layout_was
|
871
|
+
end
|
872
|
+
|
873
|
+
# render layout
|
874
|
+
if layout
|
875
|
+
extra_options = { views: views, layout: false, eat_errors: eat_errors, scope: scope }
|
876
|
+
options = options.merge(extra_options).merge!(layout_options)
|
877
|
+
|
878
|
+
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
|
879
|
+
end
|
880
|
+
|
881
|
+
if content_type
|
882
|
+
# sass-embedded returns a frozen string
|
883
|
+
output = +output
|
884
|
+
output.extend(ContentTyped).content_type = content_type
|
885
|
+
end
|
886
|
+
output
|
887
|
+
end
|
888
|
+
|
889
|
+
def compile_template(engine, data, options, views)
|
890
|
+
eat_errors = options.delete :eat_errors
|
891
|
+
template = Tilt[engine]
|
892
|
+
raise "Template engine not found: #{engine}" if template.nil?
|
893
|
+
|
894
|
+
case data
|
895
|
+
when Symbol
|
896
|
+
template_cache.fetch engine, data, options, views do
|
897
|
+
body, path, line = settings.templates[data]
|
898
|
+
if body
|
899
|
+
body = body.call if body.respond_to?(:call)
|
900
|
+
template.new(path, line.to_i, options) { body }
|
901
|
+
else
|
902
|
+
found = false
|
903
|
+
@preferred_extension = engine.to_s
|
904
|
+
find_template(views, data, template) do |file|
|
905
|
+
path ||= file # keep the initial path rather than the last one
|
906
|
+
found = File.exist?(file)
|
907
|
+
if found
|
908
|
+
path = file
|
909
|
+
break
|
910
|
+
end
|
911
|
+
end
|
912
|
+
throw :layout_missing if eat_errors && !found
|
913
|
+
template.new(path, 1, options)
|
914
|
+
end
|
915
|
+
end
|
916
|
+
when Proc
|
917
|
+
compile_block_template(template, options, &data)
|
918
|
+
when String
|
919
|
+
template_cache.fetch engine, data, options, views do
|
920
|
+
compile_block_template(template, options) { data }
|
921
|
+
end
|
922
|
+
else
|
923
|
+
raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
|
924
|
+
end
|
925
|
+
end
|
926
|
+
|
927
|
+
def compile_block_template(template, options, &body)
|
928
|
+
first_location = caller_locations.first
|
929
|
+
path = first_location.path
|
930
|
+
line = first_location.lineno
|
931
|
+
path = options[:path] || path
|
932
|
+
line = options[:line] || line
|
933
|
+
template.new(path, line.to_i, options, &body)
|
934
|
+
end
|
935
|
+
end
|
936
|
+
|
937
|
+
# Extremely simple template cache implementation.
|
938
|
+
# * Not thread-safe.
|
939
|
+
# * Size is unbounded.
|
940
|
+
# * Keys are not copied defensively, and should not be modified after
|
941
|
+
# being passed to #fetch. More specifically, the values returned by
|
942
|
+
# key#hash and key#eql? should not change.
|
943
|
+
#
|
944
|
+
# Implementation copied from Tilt::Cache.
|
945
|
+
class TemplateCache
|
946
|
+
def initialize
|
947
|
+
@cache = {}
|
948
|
+
end
|
949
|
+
|
950
|
+
# Caches a value for key, or returns the previously cached value.
|
951
|
+
# If a value has been previously cached for key then it is
|
952
|
+
# returned. Otherwise, block is yielded to and its return value
|
953
|
+
# which may be nil, is cached under key and returned.
|
954
|
+
def fetch(*key)
|
955
|
+
@cache.fetch(key) do
|
956
|
+
@cache[key] = yield
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
960
|
+
# Clears the cache.
|
961
|
+
def clear
|
962
|
+
@cache = {}
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
# Base class for all Sinatra applications and middleware.
|
967
|
+
class Base
|
968
|
+
include Rack::Utils
|
969
|
+
include Helpers
|
970
|
+
include Templates
|
971
|
+
|
972
|
+
URI_INSTANCE = URI::Parser.new
|
973
|
+
|
974
|
+
attr_accessor :app, :env, :request, :response, :params
|
975
|
+
attr_reader :template_cache
|
976
|
+
|
977
|
+
def initialize(app = nil, **_kwargs)
|
978
|
+
super()
|
979
|
+
@app = app
|
980
|
+
@template_cache = TemplateCache.new
|
981
|
+
@pinned_response = nil # whether a before! filter pinned the content-type
|
982
|
+
yield self if block_given?
|
983
|
+
end
|
984
|
+
|
985
|
+
# Rack call interface.
|
986
|
+
def call(env)
|
987
|
+
dup.call!(env)
|
988
|
+
end
|
989
|
+
|
990
|
+
def call!(env) # :nodoc:
|
991
|
+
@env = env
|
992
|
+
@params = IndifferentHash.new
|
993
|
+
@request = Request.new(env)
|
994
|
+
@response = Response.new
|
995
|
+
@pinned_response = nil
|
996
|
+
template_cache.clear if settings.reload_templates
|
997
|
+
|
998
|
+
invoke { dispatch! }
|
999
|
+
invoke { error_block!(response.status) } unless @env['sinatra.error']
|
1000
|
+
|
1001
|
+
unless @response['content-type']
|
1002
|
+
if Array === body && body[0].respond_to?(:content_type)
|
1003
|
+
content_type body[0].content_type
|
1004
|
+
elsif (default = settings.default_content_type)
|
1005
|
+
content_type default
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
@response.finish
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
# Access settings defined with Base.set.
|
1013
|
+
def self.settings
|
1014
|
+
self
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
# Access settings defined with Base.set.
|
1018
|
+
def settings
|
1019
|
+
self.class.settings
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
# Exit the current block, halts any further processing
|
1023
|
+
# of the request, and returns the specified response.
|
1024
|
+
def halt(*response)
|
1025
|
+
response = response.first if response.length == 1
|
1026
|
+
throw :halt, response
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
# Pass control to the next matching route.
|
1030
|
+
# If there are no more matching routes, Sinatra will
|
1031
|
+
# return a 404 response.
|
1032
|
+
def pass(&block)
|
1033
|
+
throw :pass, block
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
# Forward the request to the downstream app -- middleware only.
|
1037
|
+
def forward
|
1038
|
+
raise 'downstream app not set' unless @app.respond_to? :call
|
1039
|
+
|
1040
|
+
status, headers, body = @app.call env
|
1041
|
+
@response.status = status
|
1042
|
+
@response.body = body
|
1043
|
+
@response.headers.merge! headers
|
1044
|
+
nil
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
private
|
1048
|
+
|
1049
|
+
# Run filters defined on the class and all superclasses.
|
1050
|
+
# Accepts an optional block to call after each filter is applied.
|
1051
|
+
def filter!(type, base = settings, &block)
|
1052
|
+
filter!(type, base.superclass, &block) if base.superclass.respond_to?(:filters)
|
1053
|
+
base.filters[type].each do |args|
|
1054
|
+
result = process_route(*args)
|
1055
|
+
block.call(result) if block_given?
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
# Run routes defined on the class and all superclasses.
|
1060
|
+
def route!(base = settings, pass_block = nil)
|
1061
|
+
routes = base.routes[@request.request_method]
|
1062
|
+
|
1063
|
+
routes&.each do |pattern, conditions, block|
|
1064
|
+
response.delete_header('content-type') unless @pinned_response
|
1065
|
+
|
1066
|
+
returned_pass_block = process_route(pattern, conditions) do |*args|
|
1067
|
+
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
|
1068
|
+
route_eval { block[*args] }
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
# don't wipe out pass_block in superclass
|
1072
|
+
pass_block = returned_pass_block if returned_pass_block
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
# Run routes defined in superclass.
|
1076
|
+
if base.superclass.respond_to?(:routes)
|
1077
|
+
return route!(base.superclass, pass_block)
|
1078
|
+
end
|
1079
|
+
|
1080
|
+
route_eval(&pass_block) if pass_block
|
1081
|
+
route_missing
|
1082
|
+
end
|
1083
|
+
|
1084
|
+
# Run a route block and throw :halt with the result.
|
1085
|
+
def route_eval
|
1086
|
+
throw :halt, yield
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
# If the current request matches pattern and conditions, fill params
|
1090
|
+
# with keys and call the given block.
|
1091
|
+
# Revert params afterwards.
|
1092
|
+
#
|
1093
|
+
# Returns pass block.
|
1094
|
+
def process_route(pattern, conditions, block = nil, values = [])
|
1095
|
+
route = @request.path_info
|
1096
|
+
route = '/' if route.empty? && !settings.empty_path_info?
|
1097
|
+
route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
|
1098
|
+
|
1099
|
+
params = pattern.params(route)
|
1100
|
+
return unless params
|
1101
|
+
|
1102
|
+
params.delete('ignore') # TODO: better params handling, maybe turn it into "smart" object or detect changes
|
1103
|
+
force_encoding(params)
|
1104
|
+
@params = @params.merge(params) { |_k, v1, v2| v2 || v1 } if params.any?
|
1105
|
+
|
1106
|
+
regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? { |subpattern| subpattern.is_a?(Mustermann::Regular) })
|
1107
|
+
if regexp_exists
|
1108
|
+
captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
|
1109
|
+
values += captures
|
1110
|
+
@params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty?
|
1111
|
+
else
|
1112
|
+
values += params.values.flatten
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
catch(:pass) do
|
1116
|
+
conditions.each { |c| throw :pass if c.bind(self).call == false }
|
1117
|
+
block ? block[self, values] : yield(self, values)
|
1118
|
+
end
|
1119
|
+
rescue StandardError
|
1120
|
+
@env['sinatra.error.params'] = @params
|
1121
|
+
raise
|
1122
|
+
ensure
|
1123
|
+
params ||= {}
|
1124
|
+
params.each { |k, _| @params.delete(k) } unless @env['sinatra.error.params']
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
# No matching route was found or all routes passed. The default
|
1128
|
+
# implementation is to forward the request downstream when running
|
1129
|
+
# as middleware (@app is non-nil); when no downstream app is set, raise
|
1130
|
+
# a NotFound exception. Subclasses can override this method to perform
|
1131
|
+
# custom route miss logic.
|
1132
|
+
def route_missing
|
1133
|
+
raise NotFound unless @app
|
1134
|
+
|
1135
|
+
forward
|
1136
|
+
end
|
1137
|
+
|
1138
|
+
# Attempt to serve static files from public directory. Throws :halt when
|
1139
|
+
# a matching file is found, returns nil otherwise.
|
1140
|
+
def static!(options = {})
|
1141
|
+
return if (public_dir = settings.public_folder).nil?
|
1142
|
+
|
1143
|
+
path = "#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}"
|
1144
|
+
return unless valid_path?(path)
|
1145
|
+
|
1146
|
+
path = File.expand_path(path)
|
1147
|
+
return unless path.start_with?("#{File.expand_path(public_dir)}/")
|
1148
|
+
|
1149
|
+
return unless File.file?(path)
|
1150
|
+
|
1151
|
+
env['sinatra.static_file'] = path
|
1152
|
+
cache_control(*settings.static_cache_control) if settings.static_cache_control?
|
1153
|
+
send_file path, options.merge(disposition: nil)
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
# Run the block with 'throw :halt' support and apply result to the response.
|
1157
|
+
def invoke(&block)
|
1158
|
+
res = catch(:halt, &block)
|
1159
|
+
|
1160
|
+
res = [res] if (Integer === res) || (String === res)
|
1161
|
+
if (Array === res) && (Integer === res.first)
|
1162
|
+
res = res.dup
|
1163
|
+
status(res.shift)
|
1164
|
+
body(res.pop)
|
1165
|
+
headers(*res)
|
1166
|
+
elsif res.respond_to? :each
|
1167
|
+
body res
|
1168
|
+
end
|
1169
|
+
nil # avoid double setting the same response tuple twice
|
1170
|
+
end
|
1171
|
+
|
1172
|
+
# Dispatch a request with error handling.
|
1173
|
+
def dispatch!
|
1174
|
+
# Avoid passing frozen string in force_encoding
|
1175
|
+
@params.merge!(@request.params).each do |key, val|
|
1176
|
+
next unless val.respond_to?(:force_encoding)
|
1177
|
+
|
1178
|
+
val = val.dup if val.frozen?
|
1179
|
+
@params[key] = force_encoding(val)
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
invoke do
|
1183
|
+
static! if settings.static? && (request.get? || request.head?)
|
1184
|
+
filter! :before do
|
1185
|
+
@pinned_response = !response['content-type'].nil?
|
1186
|
+
end
|
1187
|
+
route!
|
1188
|
+
end
|
1189
|
+
rescue ::Exception => e
|
1190
|
+
invoke { handle_exception!(e) }
|
1191
|
+
ensure
|
1192
|
+
begin
|
1193
|
+
filter! :after unless env['sinatra.static_file']
|
1194
|
+
rescue ::Exception => e
|
1195
|
+
invoke { handle_exception!(e) } unless @env['sinatra.error']
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
# Error handling during requests.
|
1200
|
+
def handle_exception!(boom)
|
1201
|
+
error_params = @env['sinatra.error.params']
|
1202
|
+
|
1203
|
+
@params = @params.merge(error_params) if error_params
|
1204
|
+
|
1205
|
+
@env['sinatra.error'] = boom
|
1206
|
+
|
1207
|
+
http_status = if boom.is_a? Sinatra::Error
|
1208
|
+
if boom.respond_to? :http_status
|
1209
|
+
boom.http_status
|
1210
|
+
elsif settings.use_code? && boom.respond_to?(:code)
|
1211
|
+
boom.code
|
1212
|
+
end
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
http_status = 500 unless http_status&.between?(400, 599)
|
1216
|
+
status(http_status)
|
1217
|
+
|
1218
|
+
if server_error?
|
1219
|
+
dump_errors! boom if settings.dump_errors?
|
1220
|
+
raise boom if settings.show_exceptions? && (settings.show_exceptions != :after_handler)
|
1221
|
+
elsif not_found?
|
1222
|
+
headers['X-Cascade'] = 'pass' if settings.x_cascade?
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
if (res = error_block!(boom.class, boom) || error_block!(status, boom))
|
1226
|
+
return res
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
if not_found? || bad_request?
|
1230
|
+
if boom.message && boom.message != boom.class.name
|
1231
|
+
body Rack::Utils.escape_html(boom.message)
|
1232
|
+
else
|
1233
|
+
content_type 'text/html'
|
1234
|
+
body "<h1>#{not_found? ? 'Not Found' : 'Bad Request'}</h1>"
|
1235
|
+
end
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
return unless server_error?
|
1239
|
+
|
1240
|
+
raise boom if settings.raise_errors? || settings.show_exceptions?
|
1241
|
+
|
1242
|
+
error_block! Exception, boom
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
# Find an custom error block for the key(s) specified.
|
1246
|
+
def error_block!(key, *block_params)
|
1247
|
+
base = settings
|
1248
|
+
while base.respond_to?(:errors)
|
1249
|
+
args_array = base.errors[key]
|
1250
|
+
|
1251
|
+
next base = base.superclass unless args_array
|
1252
|
+
|
1253
|
+
args_array.reverse_each do |args|
|
1254
|
+
first = args == args_array.first
|
1255
|
+
args += [block_params]
|
1256
|
+
resp = process_route(*args)
|
1257
|
+
return resp unless resp.nil? && !first
|
1258
|
+
end
|
1259
|
+
end
|
1260
|
+
return false unless key.respond_to?(:superclass) && (key.superclass < Exception)
|
1261
|
+
|
1262
|
+
error_block!(key.superclass, *block_params)
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
def dump_errors!(boom)
|
1266
|
+
msg = ["#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
|
1267
|
+
@env['rack.errors'].puts(msg)
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
class << self
|
1271
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
1272
|
+
%r{/sinatra(/(base|main|show_exceptions))?\.rb$}, # all sinatra code
|
1273
|
+
%r{lib/tilt.*\.rb$}, # all tilt code
|
1274
|
+
/^\(.*\)$/, # generated code
|
1275
|
+
%r{rubygems/(custom|core_ext/kernel)_require\.rb$}, # rubygems require hacks
|
1276
|
+
/active_support/, # active_support require hacks
|
1277
|
+
%r{bundler(/(?:runtime|inline))?\.rb}, # bundler require hacks
|
1278
|
+
/<internal:/, # internal in ruby >= 1.9.2
|
1279
|
+
%r{zeitwerk/kernel\.rb} # Zeitwerk kernel#require decorator
|
1280
|
+
].freeze
|
1281
|
+
|
1282
|
+
attr_reader :routes, :filters, :templates, :errors, :on_start_callback, :on_stop_callback
|
1283
|
+
|
1284
|
+
def callers_to_ignore
|
1285
|
+
CALLERS_TO_IGNORE
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
# Removes all routes, filters, middleware and extension hooks from the
|
1289
|
+
# current class (not routes/filters/... defined by its superclass).
|
1290
|
+
def reset!
|
1291
|
+
@conditions = []
|
1292
|
+
@routes = {}
|
1293
|
+
@filters = { before: [], after: [] }
|
1294
|
+
@errors = {}
|
1295
|
+
@middleware = []
|
1296
|
+
@prototype = nil
|
1297
|
+
@extensions = []
|
1298
|
+
|
1299
|
+
@templates = if superclass.respond_to?(:templates)
|
1300
|
+
Hash.new { |_hash, key| superclass.templates[key] }
|
1301
|
+
else
|
1302
|
+
{}
|
1303
|
+
end
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
# Extension modules registered on this class and all superclasses.
|
1307
|
+
def extensions
|
1308
|
+
if superclass.respond_to?(:extensions)
|
1309
|
+
(@extensions + superclass.extensions).uniq
|
1310
|
+
else
|
1311
|
+
@extensions
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
# Middleware used in this class and all superclasses.
|
1316
|
+
def middleware
|
1317
|
+
if superclass.respond_to?(:middleware)
|
1318
|
+
superclass.middleware + @middleware
|
1319
|
+
else
|
1320
|
+
@middleware
|
1321
|
+
end
|
1322
|
+
end
|
1323
|
+
|
1324
|
+
# Sets an option to the given value. If the value is a proc,
|
1325
|
+
# the proc will be called every time the option is accessed.
|
1326
|
+
def set(option, value = (not_set = true), ignore_setter = false, &block)
|
1327
|
+
raise ArgumentError if block && !not_set
|
1328
|
+
|
1329
|
+
if block
|
1330
|
+
value = block
|
1331
|
+
not_set = false
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
if not_set
|
1335
|
+
raise ArgumentError unless option.respond_to?(:each)
|
1336
|
+
|
1337
|
+
option.each { |k, v| set(k, v) }
|
1338
|
+
return self
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
if respond_to?("#{option}=") && !ignore_setter
|
1342
|
+
return __send__("#{option}=", value)
|
1343
|
+
end
|
1344
|
+
|
1345
|
+
setter = proc { |val| set option, val, true }
|
1346
|
+
getter = proc { value }
|
1347
|
+
|
1348
|
+
case value
|
1349
|
+
when Proc
|
1350
|
+
getter = value
|
1351
|
+
when Symbol, Integer, FalseClass, TrueClass, NilClass
|
1352
|
+
getter = value.inspect
|
1353
|
+
when Hash
|
1354
|
+
setter = proc do |val|
|
1355
|
+
val = value.merge val if Hash === val
|
1356
|
+
set option, val, true
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
define_singleton("#{option}=", setter)
|
1361
|
+
define_singleton(option, getter)
|
1362
|
+
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
|
1363
|
+
self
|
1364
|
+
end
|
1365
|
+
|
1366
|
+
# Same as calling `set :option, true` for each of the given options.
|
1367
|
+
def enable(*opts)
|
1368
|
+
opts.each { |key| set(key, true) }
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
# Same as calling `set :option, false` for each of the given options.
|
1372
|
+
def disable(*opts)
|
1373
|
+
opts.each { |key| set(key, false) }
|
1374
|
+
end
|
1375
|
+
|
1376
|
+
# Define a custom error handler. Optionally takes either an Exception
|
1377
|
+
# class, or an HTTP status code to specify which errors should be
|
1378
|
+
# handled.
|
1379
|
+
def error(*codes, &block)
|
1380
|
+
args = compile! 'ERROR', /.*/, block
|
1381
|
+
codes = codes.flat_map(&method(:Array))
|
1382
|
+
codes << Exception if codes.empty?
|
1383
|
+
codes << Sinatra::NotFound if codes.include?(404)
|
1384
|
+
codes.each { |c| (@errors[c] ||= []) << args }
|
1385
|
+
end
|
1386
|
+
|
1387
|
+
# Sugar for `error(404) { ... }`
|
1388
|
+
def not_found(&block)
|
1389
|
+
error(404, &block)
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
# Define a named template. The block must return the template source.
|
1393
|
+
def template(name, &block)
|
1394
|
+
filename, line = caller_locations.first
|
1395
|
+
templates[name] = [block, filename, line.to_i]
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
# Define the layout template. The block must return the template source.
|
1399
|
+
def layout(name = :layout, &block)
|
1400
|
+
template name, &block
|
1401
|
+
end
|
1402
|
+
|
1403
|
+
# Load embedded templates from the file; uses the caller's __FILE__
|
1404
|
+
# when no file is specified.
|
1405
|
+
def inline_templates=(file = nil)
|
1406
|
+
file = (caller_files.first || File.expand_path($0)) if file.nil? || file == true
|
1407
|
+
|
1408
|
+
begin
|
1409
|
+
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
|
1410
|
+
app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2)
|
1411
|
+
rescue Errno::ENOENT
|
1412
|
+
app, data = nil
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
return unless data
|
1416
|
+
|
1417
|
+
encoding = if app && app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
|
1418
|
+
$2
|
1419
|
+
else
|
1420
|
+
settings.default_encoding
|
1421
|
+
end
|
1422
|
+
|
1423
|
+
lines = app.count("\n") + 1
|
1424
|
+
template = nil
|
1425
|
+
force_encoding data, encoding
|
1426
|
+
data.each_line do |line|
|
1427
|
+
lines += 1
|
1428
|
+
if line =~ /^@@\s*(.*\S)\s*$/
|
1429
|
+
template = force_encoding(String.new, encoding)
|
1430
|
+
templates[$1.to_sym] = [template, file, lines]
|
1431
|
+
elsif template
|
1432
|
+
template << line
|
1433
|
+
end
|
1434
|
+
end
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
# Lookup or register a mime type in Rack's mime registry.
|
1438
|
+
def mime_type(type, value = nil)
|
1439
|
+
return type if type.nil?
|
1440
|
+
return type.to_s if type.to_s.include?('/')
|
1441
|
+
|
1442
|
+
type = ".#{type}" unless type.to_s[0] == '.'
|
1443
|
+
return Rack::Mime.mime_type(type, nil) unless value
|
1444
|
+
|
1445
|
+
Rack::Mime::MIME_TYPES[type] = value
|
1446
|
+
end
|
1447
|
+
|
1448
|
+
# provides all mime types matching type, including deprecated types:
|
1449
|
+
# mime_types :html # => ['text/html']
|
1450
|
+
# mime_types :js # => ['application/javascript', 'text/javascript']
|
1451
|
+
def mime_types(type)
|
1452
|
+
type = mime_type type
|
1453
|
+
if type =~ %r{^application/(xml|javascript)$}
|
1454
|
+
[type, "text/#{$1}"]
|
1455
|
+
elsif type =~ %r{^text/(xml|javascript)$}
|
1456
|
+
[type, "application/#{$1}"]
|
1457
|
+
else
|
1458
|
+
[type]
|
1459
|
+
end
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
# Define a before filter; runs before all requests within the same
|
1463
|
+
# context as route handlers and may access/modify the request and
|
1464
|
+
# response.
|
1465
|
+
def before(path = /.*/, **options, &block)
|
1466
|
+
add_filter(:before, path, **options, &block)
|
1467
|
+
end
|
1468
|
+
|
1469
|
+
# Define an after filter; runs after all requests within the same
|
1470
|
+
# context as route handlers and may access/modify the request and
|
1471
|
+
# response.
|
1472
|
+
def after(path = /.*/, **options, &block)
|
1473
|
+
add_filter(:after, path, **options, &block)
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
# add a filter
|
1477
|
+
def add_filter(type, path = /.*/, **options, &block)
|
1478
|
+
filters[type] << compile!(type, path, block, **options)
|
1479
|
+
end
|
1480
|
+
|
1481
|
+
def on_start(&on_start_callback)
|
1482
|
+
@on_start_callback = on_start_callback
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
def on_stop(&on_stop_callback)
|
1486
|
+
@on_stop_callback = on_stop_callback
|
1487
|
+
end
|
1488
|
+
|
1489
|
+
# Add a route condition. The route is considered non-matching when the
|
1490
|
+
# block returns false.
|
1491
|
+
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
|
1492
|
+
@conditions << generate_method(name, &block)
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
def public=(value)
|
1496
|
+
warn_for_deprecation ':public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead'
|
1497
|
+
set(:public_folder, value)
|
1498
|
+
end
|
1499
|
+
|
1500
|
+
def public_dir=(value)
|
1501
|
+
self.public_folder = value
|
1502
|
+
end
|
1503
|
+
|
1504
|
+
def public_dir
|
1505
|
+
public_folder
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
# Defining a `GET` handler also automatically defines
|
1509
|
+
# a `HEAD` handler.
|
1510
|
+
def get(path, opts = {}, &block)
|
1511
|
+
conditions = @conditions.dup
|
1512
|
+
route('GET', path, opts, &block)
|
1513
|
+
|
1514
|
+
@conditions = conditions
|
1515
|
+
route('HEAD', path, opts, &block)
|
1516
|
+
end
|
1517
|
+
|
1518
|
+
def put(path, opts = {}, &block) route 'PUT', path, opts, &block end
|
1519
|
+
|
1520
|
+
def post(path, opts = {}, &block) route 'POST', path, opts, &block end
|
1521
|
+
|
1522
|
+
def delete(path, opts = {}, &block) route 'DELETE', path, opts, &block end
|
1523
|
+
|
1524
|
+
def head(path, opts = {}, &block) route 'HEAD', path, opts, &block end
|
1525
|
+
|
1526
|
+
def options(path, opts = {}, &block) route 'OPTIONS', path, opts, &block end
|
1527
|
+
|
1528
|
+
def patch(path, opts = {}, &block) route 'PATCH', path, opts, &block end
|
1529
|
+
|
1530
|
+
def link(path, opts = {}, &block) route 'LINK', path, opts, &block end
|
1531
|
+
|
1532
|
+
def unlink(path, opts = {}, &block) route 'UNLINK', path, opts, &block end
|
1533
|
+
|
1534
|
+
# Makes the methods defined in the block and in the Modules given
|
1535
|
+
# in `extensions` available to the handlers and templates
|
1536
|
+
def helpers(*extensions, &block)
|
1537
|
+
class_eval(&block) if block_given?
|
1538
|
+
include(*extensions) if extensions.any?
|
1539
|
+
end
|
1540
|
+
|
1541
|
+
# Register an extension. Alternatively take a block from which an
|
1542
|
+
# extension will be created and registered on the fly.
|
1543
|
+
def register(*extensions, &block)
|
1544
|
+
extensions << Module.new(&block) if block_given?
|
1545
|
+
@extensions += extensions
|
1546
|
+
extensions.each do |extension|
|
1547
|
+
extend extension
|
1548
|
+
extension.registered(self) if extension.respond_to?(:registered)
|
1549
|
+
end
|
1550
|
+
end
|
1551
|
+
|
1552
|
+
def development?; environment == :development end
|
1553
|
+
def production?; environment == :production end
|
1554
|
+
def test?; environment == :test end
|
1555
|
+
|
1556
|
+
# Set configuration options for Sinatra and/or the app.
|
1557
|
+
# Allows scoping of settings for certain environments.
|
1558
|
+
def configure(*envs)
|
1559
|
+
yield self if envs.empty? || envs.include?(environment.to_sym)
|
1560
|
+
end
|
1561
|
+
|
1562
|
+
# Use the specified Rack middleware
|
1563
|
+
def use(middleware, *args, &block)
|
1564
|
+
@prototype = nil
|
1565
|
+
@middleware << [middleware, args, block]
|
1566
|
+
end
|
1567
|
+
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
|
1568
|
+
|
1569
|
+
# Stop the self-hosted server if running.
|
1570
|
+
def quit!
|
1571
|
+
return unless running?
|
1572
|
+
|
1573
|
+
# Use Thin's hard #stop! if available, otherwise just #stop.
|
1574
|
+
running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop
|
1575
|
+
warn '== Sinatra has ended his set (crowd applauds)' unless suppress_messages?
|
1576
|
+
set :running_server, nil
|
1577
|
+
set :handler_name, nil
|
1578
|
+
|
1579
|
+
on_stop_callback.call unless on_stop_callback.nil?
|
1580
|
+
end
|
1581
|
+
|
1582
|
+
alias stop! quit!
|
1583
|
+
|
1584
|
+
# Run the Sinatra app as a self-hosted server using
|
1585
|
+
# Puma, Falcon, or WEBrick (in that order). If given a block, will call
|
1586
|
+
# with the constructed handler once we have taken the stage.
|
1587
|
+
def run!(options = {}, &block)
|
1588
|
+
return if running?
|
1589
|
+
|
1590
|
+
set options
|
1591
|
+
handler = Rackup::Handler.pick(server)
|
1592
|
+
handler_name = handler.name.gsub(/.*::/, '')
|
1593
|
+
server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
|
1594
|
+
server_settings.merge!(Port: port, Host: bind)
|
1595
|
+
|
1596
|
+
begin
|
1597
|
+
start_server(handler, server_settings, handler_name, &block)
|
1598
|
+
rescue Errno::EADDRINUSE
|
1599
|
+
warn "== Someone is already performing on port #{port}!"
|
1600
|
+
raise
|
1601
|
+
ensure
|
1602
|
+
quit!
|
1603
|
+
end
|
1604
|
+
end
|
1605
|
+
|
1606
|
+
alias start! run!
|
1607
|
+
|
1608
|
+
# Check whether the self-hosted server is running or not.
|
1609
|
+
def running?
|
1610
|
+
running_server?
|
1611
|
+
end
|
1612
|
+
|
1613
|
+
# The prototype instance used to process requests.
|
1614
|
+
def prototype
|
1615
|
+
@prototype ||= new
|
1616
|
+
end
|
1617
|
+
|
1618
|
+
# Create a new instance without middleware in front of it.
|
1619
|
+
alias new! new unless method_defined? :new!
|
1620
|
+
|
1621
|
+
# Create a new instance of the class fronted by its middleware
|
1622
|
+
# pipeline. The object is guaranteed to respond to #call but may not be
|
1623
|
+
# an instance of the class new was called on.
|
1624
|
+
def new(*args, &block)
|
1625
|
+
instance = new!(*args, &block)
|
1626
|
+
Wrapper.new(build(instance).to_app, instance)
|
1627
|
+
end
|
1628
|
+
ruby2_keywords :new if respond_to?(:ruby2_keywords, true)
|
1629
|
+
|
1630
|
+
# Creates a Rack::Builder instance with all the middleware set up and
|
1631
|
+
# the given +app+ as end point.
|
1632
|
+
def build(app)
|
1633
|
+
builder = Rack::Builder.new
|
1634
|
+
setup_default_middleware builder
|
1635
|
+
setup_middleware builder
|
1636
|
+
builder.run app
|
1637
|
+
builder
|
1638
|
+
end
|
1639
|
+
|
1640
|
+
def call(env)
|
1641
|
+
synchronize { prototype.call(env) }
|
1642
|
+
end
|
1643
|
+
|
1644
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
1645
|
+
# line / method information; the resulting array contains filenames only.
|
1646
|
+
def caller_files
|
1647
|
+
cleaned_caller(1).flatten
|
1648
|
+
end
|
1649
|
+
|
1650
|
+
private
|
1651
|
+
|
1652
|
+
# Starts the server by running the Rack Handler.
|
1653
|
+
def start_server(handler, server_settings, handler_name)
|
1654
|
+
# Ensure we initialize middleware before startup, to match standard Rack
|
1655
|
+
# behavior, by ensuring an instance exists:
|
1656
|
+
prototype
|
1657
|
+
# Run the instance we created:
|
1658
|
+
handler.run(self, **server_settings) do |server|
|
1659
|
+
unless suppress_messages?
|
1660
|
+
warn "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
setup_traps
|
1664
|
+
set :running_server, server
|
1665
|
+
set :handler_name, handler_name
|
1666
|
+
server.threaded = settings.threaded if server.respond_to? :threaded=
|
1667
|
+
on_start_callback.call unless on_start_callback.nil?
|
1668
|
+
yield server if block_given?
|
1669
|
+
end
|
1670
|
+
end
|
1671
|
+
|
1672
|
+
def suppress_messages?
|
1673
|
+
handler_name =~ /cgi/i || quiet
|
1674
|
+
end
|
1675
|
+
|
1676
|
+
def setup_traps
|
1677
|
+
return unless traps?
|
1678
|
+
|
1679
|
+
at_exit { quit! }
|
1680
|
+
|
1681
|
+
%i[INT TERM].each do |signal|
|
1682
|
+
old_handler = trap(signal) do
|
1683
|
+
quit!
|
1684
|
+
old_handler.call if old_handler.respond_to?(:call)
|
1685
|
+
end
|
1686
|
+
end
|
1687
|
+
|
1688
|
+
set :traps, false
|
1689
|
+
end
|
1690
|
+
|
1691
|
+
# Dynamically defines a method on settings.
|
1692
|
+
def define_singleton(name, content = Proc.new)
|
1693
|
+
singleton_class.class_eval do
|
1694
|
+
undef_method(name) if method_defined? name
|
1695
|
+
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
|
1696
|
+
end
|
1697
|
+
end
|
1698
|
+
|
1699
|
+
# Condition for matching host name. Parameter might be String or Regexp.
|
1700
|
+
def host_name(pattern)
|
1701
|
+
condition { pattern === request.host }
|
1702
|
+
end
|
1703
|
+
|
1704
|
+
# Condition for matching user agent. Parameter should be Regexp.
|
1705
|
+
# Will set params[:agent].
|
1706
|
+
def user_agent(pattern)
|
1707
|
+
condition do
|
1708
|
+
if request.user_agent.to_s =~ pattern
|
1709
|
+
@params[:agent] = $~[1..-1]
|
1710
|
+
true
|
1711
|
+
else
|
1712
|
+
false
|
1713
|
+
end
|
1714
|
+
end
|
1715
|
+
end
|
1716
|
+
alias agent user_agent
|
1717
|
+
|
1718
|
+
# Condition for matching mimetypes. Accepts file extensions.
|
1719
|
+
def provides(*types)
|
1720
|
+
types.map! { |t| mime_types(t) }
|
1721
|
+
types.flatten!
|
1722
|
+
condition do
|
1723
|
+
response_content_type = response['content-type']
|
1724
|
+
preferred_type = request.preferred_type(types)
|
1725
|
+
|
1726
|
+
if response_content_type
|
1727
|
+
types.include?(response_content_type) || types.include?(response_content_type[/^[^;]+/])
|
1728
|
+
elsif preferred_type
|
1729
|
+
params = (preferred_type.respond_to?(:params) ? preferred_type.params : {})
|
1730
|
+
content_type(preferred_type, params)
|
1731
|
+
true
|
1732
|
+
else
|
1733
|
+
false
|
1734
|
+
end
|
1735
|
+
end
|
1736
|
+
end
|
1737
|
+
|
1738
|
+
def route(verb, path, options = {}, &block)
|
1739
|
+
enable :empty_path_info if path == '' && empty_path_info.nil?
|
1740
|
+
signature = compile!(verb, path, block, **options)
|
1741
|
+
(@routes[verb] ||= []) << signature
|
1742
|
+
invoke_hook(:route_added, verb, path, block)
|
1743
|
+
signature
|
1744
|
+
end
|
1745
|
+
|
1746
|
+
def invoke_hook(name, *args)
|
1747
|
+
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
def generate_method(method_name, &block)
|
1751
|
+
define_method(method_name, &block)
|
1752
|
+
method = instance_method method_name
|
1753
|
+
remove_method method_name
|
1754
|
+
method
|
1755
|
+
end
|
1756
|
+
|
1757
|
+
def compile!(verb, path, block, **options)
|
1758
|
+
# Because of self.options.host
|
1759
|
+
host_name(options.delete(:host)) if options.key?(:host)
|
1760
|
+
# Pass Mustermann opts to compile()
|
1761
|
+
route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze
|
1762
|
+
|
1763
|
+
options.each_pair { |option, args| send(option, *args) }
|
1764
|
+
|
1765
|
+
pattern = compile(path, route_mustermann_opts)
|
1766
|
+
method_name = "#{verb} #{path}"
|
1767
|
+
unbound_method = generate_method(method_name, &block)
|
1768
|
+
conditions = @conditions
|
1769
|
+
@conditions = []
|
1770
|
+
wrapper = block.arity.zero? ?
|
1771
|
+
proc { |a, _p| unbound_method.bind(a).call } :
|
1772
|
+
proc { |a, p| unbound_method.bind(a).call(*p) }
|
1773
|
+
|
1774
|
+
[pattern, conditions, wrapper]
|
1775
|
+
end
|
1776
|
+
|
1777
|
+
def compile(path, route_mustermann_opts = {})
|
1778
|
+
Mustermann.new(path, **mustermann_opts.merge(route_mustermann_opts))
|
1779
|
+
end
|
1780
|
+
|
1781
|
+
def setup_default_middleware(builder)
|
1782
|
+
builder.use ExtendedRack
|
1783
|
+
builder.use ShowExceptions if show_exceptions?
|
1784
|
+
builder.use Rack::MethodOverride if method_override?
|
1785
|
+
builder.use Rack::Head
|
1786
|
+
setup_logging builder
|
1787
|
+
setup_sessions builder
|
1788
|
+
setup_protection builder
|
1789
|
+
end
|
1790
|
+
|
1791
|
+
def setup_middleware(builder)
|
1792
|
+
middleware.each { |c, a, b| builder.use(c, *a, &b) }
|
1793
|
+
end
|
1794
|
+
|
1795
|
+
def setup_logging(builder)
|
1796
|
+
if logging?
|
1797
|
+
setup_common_logger(builder)
|
1798
|
+
setup_custom_logger(builder)
|
1799
|
+
elsif logging == false
|
1800
|
+
setup_null_logger(builder)
|
1801
|
+
end
|
1802
|
+
end
|
1803
|
+
|
1804
|
+
def setup_null_logger(builder)
|
1805
|
+
builder.use Rack::NullLogger
|
1806
|
+
end
|
1807
|
+
|
1808
|
+
def setup_common_logger(builder)
|
1809
|
+
builder.use Sinatra::CommonLogger
|
1810
|
+
end
|
1811
|
+
|
1812
|
+
def setup_custom_logger(builder)
|
1813
|
+
if logging.respond_to? :to_int
|
1814
|
+
builder.use Rack::Logger, logging
|
1815
|
+
else
|
1816
|
+
builder.use Rack::Logger
|
1817
|
+
end
|
1818
|
+
end
|
1819
|
+
|
1820
|
+
def setup_protection(builder)
|
1821
|
+
return unless protection?
|
1822
|
+
|
1823
|
+
options = Hash === protection ? protection.dup : {}
|
1824
|
+
options = {
|
1825
|
+
img_src: "'self' data:",
|
1826
|
+
font_src: "'self'"
|
1827
|
+
}.merge options
|
1828
|
+
|
1829
|
+
protect_session = options.fetch(:session) { sessions? }
|
1830
|
+
options[:without_session] = !protect_session
|
1831
|
+
|
1832
|
+
options[:reaction] ||= :drop_session
|
1833
|
+
|
1834
|
+
builder.use Rack::Protection, options
|
1835
|
+
end
|
1836
|
+
|
1837
|
+
def setup_sessions(builder)
|
1838
|
+
return unless sessions?
|
1839
|
+
|
1840
|
+
options = {}
|
1841
|
+
options[:secret] = session_secret if session_secret?
|
1842
|
+
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
|
1843
|
+
builder.use session_store, options
|
1844
|
+
end
|
1845
|
+
|
1846
|
+
def inherited(subclass)
|
1847
|
+
subclass.reset!
|
1848
|
+
subclass.set :app_file, caller_files.first unless subclass.app_file?
|
1849
|
+
super
|
1850
|
+
end
|
1851
|
+
|
1852
|
+
@@mutex = Mutex.new
|
1853
|
+
def synchronize(&block)
|
1854
|
+
if lock?
|
1855
|
+
@@mutex.synchronize(&block)
|
1856
|
+
else
|
1857
|
+
yield
|
1858
|
+
end
|
1859
|
+
end
|
1860
|
+
|
1861
|
+
# used for deprecation warnings
|
1862
|
+
def warn_for_deprecation(message)
|
1863
|
+
warn message + "\n\tfrom #{cleaned_caller.first.join(':')}"
|
1864
|
+
end
|
1865
|
+
|
1866
|
+
# Like Kernel#caller but excluding certain magic entries
|
1867
|
+
def cleaned_caller(keep = 3)
|
1868
|
+
caller(1)
|
1869
|
+
.map! { |line| line.split(/:(?=\d|in )/, 3)[0, keep] }
|
1870
|
+
.reject { |file, *_| callers_to_ignore.any? { |pattern| file =~ pattern } }
|
1871
|
+
end
|
1872
|
+
end
|
1873
|
+
|
1874
|
+
# Force data to specified encoding. It defaults to settings.default_encoding
|
1875
|
+
# which is UTF-8 by default
|
1876
|
+
def self.force_encoding(data, encoding = default_encoding)
|
1877
|
+
return if data == settings || data.is_a?(Tempfile)
|
1878
|
+
|
1879
|
+
if data.respond_to? :force_encoding
|
1880
|
+
data.force_encoding(encoding).encode!
|
1881
|
+
elsif data.respond_to? :each_value
|
1882
|
+
data.each_value { |v| force_encoding(v, encoding) }
|
1883
|
+
elsif data.respond_to? :each
|
1884
|
+
data.each { |v| force_encoding(v, encoding) }
|
1885
|
+
end
|
1886
|
+
data
|
1887
|
+
end
|
1888
|
+
|
1889
|
+
def force_encoding(*args)
|
1890
|
+
settings.force_encoding(*args)
|
1891
|
+
end
|
1892
|
+
|
1893
|
+
reset!
|
1894
|
+
|
1895
|
+
set :environment, (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
|
1896
|
+
set :raise_errors, proc { test? }
|
1897
|
+
set :dump_errors, proc { !test? }
|
1898
|
+
set :show_exceptions, proc { development? }
|
1899
|
+
set :sessions, false
|
1900
|
+
set :session_store, Rack::Protection::EncryptedCookie
|
1901
|
+
set :logging, false
|
1902
|
+
set :protection, true
|
1903
|
+
set :method_override, false
|
1904
|
+
set :use_code, false
|
1905
|
+
set :default_encoding, 'utf-8'
|
1906
|
+
set :x_cascade, true
|
1907
|
+
set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
|
1908
|
+
settings.add_charset << %r{^text/}
|
1909
|
+
set :mustermann_opts, {}
|
1910
|
+
set :default_content_type, 'text/html'
|
1911
|
+
|
1912
|
+
# explicitly generating a session secret eagerly to play nice with preforking
|
1913
|
+
begin
|
1914
|
+
require 'securerandom'
|
1915
|
+
set :session_secret, SecureRandom.hex(64)
|
1916
|
+
rescue LoadError, NotImplementedError, RuntimeError
|
1917
|
+
# SecureRandom raises a NotImplementedError if no random device is available
|
1918
|
+
# RuntimeError raised due to broken openssl backend: https://bugs.ruby-lang.org/issues/19230
|
1919
|
+
set :session_secret, format('%064x', Kernel.rand((2**256) - 1))
|
1920
|
+
end
|
1921
|
+
|
1922
|
+
class << self
|
1923
|
+
alias methodoverride? method_override?
|
1924
|
+
alias methodoverride= method_override=
|
1925
|
+
end
|
1926
|
+
|
1927
|
+
set :run, false # start server via at-exit hook?
|
1928
|
+
set :running_server, nil
|
1929
|
+
set :handler_name, nil
|
1930
|
+
set :traps, true
|
1931
|
+
set :server, %w[HTTP webrick]
|
1932
|
+
set :bind, proc { development? ? 'localhost' : '0.0.0.0' }
|
1933
|
+
set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
|
1934
|
+
set :quiet, false
|
1935
|
+
|
1936
|
+
ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE
|
1937
|
+
|
1938
|
+
server.unshift 'thin' if ruby_engine != 'jruby'
|
1939
|
+
server.unshift 'falcon' if ruby_engine != 'jruby'
|
1940
|
+
server.unshift 'trinidad' if ruby_engine == 'jruby'
|
1941
|
+
server.unshift 'puma'
|
1942
|
+
|
1943
|
+
set :absolute_redirects, true
|
1944
|
+
set :prefixed_redirects, false
|
1945
|
+
set :empty_path_info, nil
|
1946
|
+
set :strict_paths, true
|
1947
|
+
|
1948
|
+
set :app_file, nil
|
1949
|
+
set :root, proc { app_file && File.expand_path(File.dirname(app_file)) }
|
1950
|
+
set :views, proc { root && File.join(root, 'views') }
|
1951
|
+
set :reload_templates, proc { development? }
|
1952
|
+
set :lock, false
|
1953
|
+
set :threaded, true
|
1954
|
+
|
1955
|
+
set :public_folder, proc { root && File.join(root, 'public') }
|
1956
|
+
set :static, proc { public_folder && File.exist?(public_folder) }
|
1957
|
+
set :static_cache_control, false
|
1958
|
+
|
1959
|
+
error ::Exception do
|
1960
|
+
response.status = 500
|
1961
|
+
content_type 'text/html'
|
1962
|
+
'<h1>Internal Server Error</h1>'
|
1963
|
+
end
|
1964
|
+
|
1965
|
+
configure :development do
|
1966
|
+
get '/__sinatra__/:image.png' do
|
1967
|
+
filename = __dir__ + "/images/#{params[:image].to_i}.png"
|
1968
|
+
content_type :png
|
1969
|
+
send_file filename
|
1970
|
+
end
|
1971
|
+
|
1972
|
+
error NotFound do
|
1973
|
+
content_type 'text/html'
|
1974
|
+
|
1975
|
+
if instance_of?(Sinatra::Application)
|
1976
|
+
code = <<-RUBY.gsub(/^ {12}/, '')
|
1977
|
+
#{request.request_method.downcase} '#{request.path_info}' do
|
1978
|
+
"Hello World"
|
1979
|
+
end
|
1980
|
+
RUBY
|
1981
|
+
else
|
1982
|
+
code = <<-RUBY.gsub(/^ {12}/, '')
|
1983
|
+
class #{self.class}
|
1984
|
+
#{request.request_method.downcase} '#{request.path_info}' do
|
1985
|
+
"Hello World"
|
1986
|
+
end
|
1987
|
+
end
|
1988
|
+
RUBY
|
1989
|
+
|
1990
|
+
file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(%r{^/}, '')
|
1991
|
+
code = "# in #{file}\n#{code}" unless file.empty?
|
1992
|
+
end
|
1993
|
+
|
1994
|
+
<<-HTML.gsub(/^ {10}/, '')
|
1995
|
+
<!DOCTYPE html>
|
1996
|
+
<html>
|
1997
|
+
<head>
|
1998
|
+
<style type="text/css">
|
1999
|
+
body { text-align:center;font-family:helvetica,arial;font-size:22px;
|
2000
|
+
color:#888;margin:20px}
|
2001
|
+
#c {margin:0 auto;width:500px;text-align:left}
|
2002
|
+
</style>
|
2003
|
+
</head>
|
2004
|
+
<body>
|
2005
|
+
<h2>Sinatra doesn’t know this ditty.</h2>
|
2006
|
+
<img src='#{uri '/__sinatra__/404.png'}'>
|
2007
|
+
<div id="c">
|
2008
|
+
Try this:
|
2009
|
+
<pre>#{Rack::Utils.escape_html(code)}</pre>
|
2010
|
+
</div>
|
2011
|
+
</body>
|
2012
|
+
</html>
|
2013
|
+
HTML
|
2014
|
+
end
|
2015
|
+
end
|
2016
|
+
end
|
2017
|
+
|
2018
|
+
# Execution context for classic style (top-level) applications. All
|
2019
|
+
# DSL methods executed on main are delegated to this class.
|
2020
|
+
#
|
2021
|
+
# The Application class should not be subclassed, unless you want to
|
2022
|
+
# inherit all settings, routes, handlers, and error pages from the
|
2023
|
+
# top-level. Subclassing Sinatra::Base is highly recommended for
|
2024
|
+
# modular applications.
|
2025
|
+
class Application < Base
|
2026
|
+
set :logging, proc { !test? }
|
2027
|
+
set :method_override, true
|
2028
|
+
set :run, proc { !test? }
|
2029
|
+
set :app_file, nil
|
2030
|
+
|
2031
|
+
def self.register(*extensions, &block) # :nodoc:
|
2032
|
+
added_methods = extensions.flat_map(&:public_instance_methods)
|
2033
|
+
Delegator.delegate(*added_methods)
|
2034
|
+
super(*extensions, &block)
|
2035
|
+
end
|
2036
|
+
end
|
2037
|
+
|
2038
|
+
# Sinatra delegation mixin. Mixing this module into an object causes all
|
2039
|
+
# methods to be delegated to the Sinatra::Application class. Used primarily
|
2040
|
+
# at the top-level.
|
2041
|
+
module Delegator # :nodoc:
|
2042
|
+
def self.delegate(*methods)
|
2043
|
+
methods.each do |method_name|
|
2044
|
+
define_method(method_name) do |*args, &block|
|
2045
|
+
return super(*args, &block) if respond_to? method_name
|
2046
|
+
|
2047
|
+
Delegator.target.send(method_name, *args, &block)
|
2048
|
+
end
|
2049
|
+
# ensure keyword argument passing is compatible with ruby >= 2.7
|
2050
|
+
ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true)
|
2051
|
+
private method_name
|
2052
|
+
end
|
2053
|
+
end
|
2054
|
+
|
2055
|
+
delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
|
2056
|
+
:template, :layout, :before, :after, :error, :not_found, :configure,
|
2057
|
+
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
|
2058
|
+
:production?, :helpers, :settings, :register, :on_start, :on_stop
|
2059
|
+
|
2060
|
+
class << self
|
2061
|
+
attr_accessor :target
|
2062
|
+
end
|
2063
|
+
|
2064
|
+
self.target = Application
|
2065
|
+
end
|
2066
|
+
|
2067
|
+
class Wrapper
|
2068
|
+
def initialize(stack, instance)
|
2069
|
+
@stack = stack
|
2070
|
+
@instance = instance
|
2071
|
+
end
|
2072
|
+
|
2073
|
+
def settings
|
2074
|
+
@instance.settings
|
2075
|
+
end
|
2076
|
+
|
2077
|
+
def helpers
|
2078
|
+
@instance
|
2079
|
+
end
|
2080
|
+
|
2081
|
+
def call(env)
|
2082
|
+
@stack.call(env)
|
2083
|
+
end
|
2084
|
+
|
2085
|
+
def inspect
|
2086
|
+
"#<#{@instance.class} app_file=#{settings.app_file.inspect}>"
|
2087
|
+
end
|
2088
|
+
end
|
2089
|
+
|
2090
|
+
# Create a new Sinatra application; the block is evaluated in the class scope.
|
2091
|
+
def self.new(base = Base, &block)
|
2092
|
+
base = Class.new(base)
|
2093
|
+
base.class_eval(&block) if block_given?
|
2094
|
+
base
|
2095
|
+
end
|
2096
|
+
|
2097
|
+
# Extend the top-level DSL with the modules provided.
|
2098
|
+
def self.register(*extensions, &block)
|
2099
|
+
Delegator.target.register(*extensions, &block)
|
2100
|
+
end
|
2101
|
+
|
2102
|
+
# Include the helper modules provided in Sinatra's request context.
|
2103
|
+
def self.helpers(*extensions, &block)
|
2104
|
+
Delegator.target.helpers(*extensions, &block)
|
2105
|
+
end
|
2106
|
+
|
2107
|
+
# Use the middleware for classic applications.
|
2108
|
+
def self.use(*args, &block)
|
2109
|
+
Delegator.target.use(*args, &block)
|
2110
|
+
end
|
2111
|
+
end
|