feat: add Prism, Cors and HttpRange
This commit is contained in:
1 parent
31799fc5af
commit
bb80e7d3c4
2 files changed
+161
No files matched your search
@@ -0,0 +1,103 @@
|
||||
module Prism
|
||||
# Rack::Cors-lite: a small registry of cross-origin rules, matched by
|
||||
# allowed origin then by resource path pattern. Prism::Middleware::Cors
|
||||
# reads this registry on every request; it holds no per-request state.
|
||||
module Cors
|
||||
class Resource
|
||||
attr_reader :pattern, :methods, :headers, :expose, :credentials, :max_age
|
||||
|
||||
def initialize(pattern, methods: %w[GET], headers: :any, expose: [], credentials: false, max_age: nil)
|
||||
@pattern = pattern
|
||||
@methods = Array(methods).map { |m| m.to_s.upcase }
|
||||
@headers = headers == :any ? :any : Array(headers).map(&:downcase)
|
||||
@expose = Array(expose)
|
||||
@credentials = credentials
|
||||
@max_age = max_age
|
||||
end
|
||||
|
||||
def match?(path)
|
||||
File.fnmatch(pattern, path)
|
||||
end
|
||||
|
||||
def allows_method?(method)
|
||||
methods.include?(method.to_s.upcase)
|
||||
end
|
||||
|
||||
def allows_headers?(requested)
|
||||
return true if headers == :any
|
||||
requested_list = requested.to_s.split(',').map { |h| h.strip.downcase }.reject(&:empty?)
|
||||
requested_list.all? { |h| headers.include?(h) }
|
||||
end
|
||||
end
|
||||
|
||||
class Rule
|
||||
def initialize
|
||||
@origins = []
|
||||
@resources = []
|
||||
end
|
||||
|
||||
def origins(*list)
|
||||
@origins = list.flatten
|
||||
end
|
||||
|
||||
def resource(pattern, **options)
|
||||
@resources << Resource.new(pattern, **options)
|
||||
end
|
||||
|
||||
def allows_origin?(origin)
|
||||
return false if origin.nil? || origin.empty?
|
||||
@origins.any? do |allowed|
|
||||
case allowed
|
||||
when '*' then true
|
||||
when Regexp then allowed.match?(origin)
|
||||
else allowed == origin
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resource_for(path)
|
||||
@resources.find { |resource| resource.match?(path) }
|
||||
end
|
||||
end
|
||||
|
||||
class Configuration
|
||||
attr_reader :rules
|
||||
|
||||
def initialize
|
||||
@rules = []
|
||||
end
|
||||
|
||||
def allow(&block)
|
||||
rule = Rule.new
|
||||
rule.instance_eval(&block)
|
||||
@rules << rule
|
||||
rule
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def configuration
|
||||
@configuration ||= Configuration.new
|
||||
end
|
||||
|
||||
def configure
|
||||
yield configuration
|
||||
end
|
||||
|
||||
def reset!
|
||||
@configuration = Configuration.new
|
||||
end
|
||||
|
||||
# Returns the [Rule, Resource] pair matching this origin/path, or nil.
|
||||
def resource_for(origin, path)
|
||||
configuration.rules.each do |rule|
|
||||
next unless rule.allows_origin?(origin)
|
||||
resource = rule.resource_for(path)
|
||||
next unless resource
|
||||
return [rule, resource]
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
module Prism
|
||||
# RFC 7233 single-range parsing, shared by Middleware::Static and
|
||||
# Middleware::Assets so both can serve `Range: bytes=...` requests (video
|
||||
# seeking, resumable downloads, `curl -C -`) instead of always sending the
|
||||
# whole file. Only a single byte-range-spec is honored - a request naming
|
||||
# several comma-separated ranges falls back to a normal 200 with the full
|
||||
# body, which RFC 7233 SS3.1 explicitly allows ("MAY ignore the Range
|
||||
# header field"), rather than implementing multipart/byteranges for a case
|
||||
# none of this framework's own clients ever send.
|
||||
module HttpRange
|
||||
Satisfiable = Struct.new(:start, :finish, :length, keyword_init: true)
|
||||
|
||||
SPEC = /\A(\d*)-(\d*)\z/.freeze
|
||||
|
||||
# Returns:
|
||||
# nil - no Range header, or one this server chooses to ignore
|
||||
# (multi-range) -> caller should send a normal 200.
|
||||
# :unsatisfiable - a single range was given but it names no byte that
|
||||
# exists in a file of `file_size` bytes -> caller
|
||||
# should send 416.
|
||||
# Satisfiable - a single, in-bounds range -> caller should send 206.
|
||||
def self.parse(header, file_size)
|
||||
return nil if header.nil? || !header.start_with?('bytes=')
|
||||
|
||||
specs = header.delete_prefix('bytes=').split(',', -1).map(&:strip)
|
||||
return nil if specs.size != 1
|
||||
|
||||
match = SPEC.match(specs.first)
|
||||
return :unsatisfiable unless match
|
||||
|
||||
start_s, end_s = match[1], match[2]
|
||||
return :unsatisfiable if start_s.empty? && end_s.empty?
|
||||
|
||||
if start_s.empty?
|
||||
suffix_length = end_s.to_i
|
||||
return :unsatisfiable if suffix_length <= 0
|
||||
start = [file_size - suffix_length, 0].max
|
||||
finish = file_size - 1
|
||||
else
|
||||
start = start_s.to_i
|
||||
finish = end_s.empty? ? file_size - 1 : [end_s.to_i, file_size - 1].min
|
||||
end
|
||||
|
||||
return :unsatisfiable if file_size.zero? || start >= file_size || start > finish
|
||||
|
||||
Satisfiable.new(start: start, finish: finish, length: finish - start + 1)
|
||||
end
|
||||
|
||||
# Reads only the requested slice, without loading the rest of the file
|
||||
# into memory - the entire point of Range for a large video/asset file.
|
||||
def self.read_slice(path, satisfiable)
|
||||
File.open(path, 'rb') do |f|
|
||||
f.seek(satisfiable.start)
|
||||
f.read(satisfiable.length) || String.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user