Setup Rubocop (#1537)

* Initialize rubocop

* Style/StringLiterals: prefer single quotes

* Style/AndOr: use `&&` and `||`, instead of `and` and `or`

* Style/HashSyntax: use new hash syntax

* Layout/EmptyLineAfterGuardClause: add empty lines after guard clause

* Style/SingleLineMethods: temporary disable

It breaks layout of the code, it is better to fix it manually

* Style/Proc: prefer `proc` vs `Proc.new`

* Disable Lint/AmbiguousBlockAssociation

It affects proc definitions for sinatra DSL

* Disable Style/CaseEquality

* Lint/UnusedBlockArgument: put underscore in front of it

* Style/Alias: prefer alias vs alias_method in a class body

* Layout/EmptyLineBetweenDefs: add empty lines between defs

* Style/ParallelAssignment: don't use parallel assigment

* Style/RegexpLiteral: prefer %r for regular expressions

* Naming/UncommunicativeMethodParamName: fix abbrevs

* Style/PerlBackrefs: disable cop

* Layout/SpaceAfterComma: add missing spaces

* Style/Documentation: disable cop

* Style/FrozenStringLiteralComment: add frozen_string_literal

* Layout/AlignHash: align hash

* Layout/ExtraSpacing: allow for alignment

* Layout/SpaceAroundOperators: add missing spaces

* Style/Not: prefer `!` instead of `not`

* Style/GuardClause: add guard conditions

* Style/MutableConstant: freeze contants

* Lint/IneffectiveAccessModifier: disable cop

* Lint/RescueException: disable cop

* Style/SpecialGlobalVars: disable cop

* Layout/DotPosition: fix position of dot for multiline method chains

* Layout/SpaceInsideArrayLiteralBrackets: don't use spaces inside arrays

* Layout/SpaceInsideBlockBraces: add space for blocks

* Layout/SpaceInsideHashLiteralBraces: add spaces for hashes

* Style/FormatString: use format string syntax

* Style/StderrPuts: `warn` is preferable to `$stderr.puts`

* Bundler/DuplicatedGem: disable cop

* Layout/AlignArray: fix warning

* Lint/AssignmentInCondition: remove assignments from conditions

* Layout/IndentHeredoc: disable cop

* Layout/SpaceInsideParens: remove extra spaces

* Lint/UnusedMethodArgument: put underscore in front of unused arg

* Naming/RescuedExceptionsVariableName: use `e` for exceptions

* Style/CommentedKeyword: put comments before the method

* Style/FormatStringToken: disable cop

* Style/MultilineIfModifier: move condition before the method

* Style/SignalException: prefer `raise` to `fail`

* Style/SymbolArray: prefer %i for array of symbols

* Gemspec/OrderedDependencies: Use alphabetical order for dependencies

* Lint/UselessAccessModifier: disable cop

* Naming/HeredocDelimiterNaming: change delimiter's name

* Style/ClassCheck: prefer `is_a?` to `kind_of?`

* Style/ClassVars: disable cop

* Style/Encoding: remove coding comment

* Style/RedundantParentheses: remove extra parentheses

* Style/StringLiteralsInInterpolation: prefer singl quotes

* Layout/AlignArguments: fix alignment

* Layout/ClosingHeredocIndentation: align heredoc

* Layout/EmptyLineAfterMagicComment: add empty line

* Set RubyVersion for rubocop

* Lint/UselessAssignment: disable cop

* Style/EmptyLiteral: disable cop

Causes test failures

* Minor code-style fixes with --safe-auto-correct option

* Disable the rest of the cops that cause warnings

It would be easier to re-enable them in separate PRs

* Add rubocop check to the default Rake task

* Update to rubocop 1.32.0

* Rubocop updates for rack-protection and sinatra-contrib

* Disable Style/SlicingWithRange cop

* Make suggested updates

Co-authored-by: Jordan Owens <jkowens@gmail.com>
This commit is contained in:
Vasiliy 2022-07-31 14:56:44 +02:00 committed by GitHub
parent 9a85bbfcb2
commit 8ae87a87f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 1600 additions and 1142 deletions

151
.rubocop.yml Normal file
View File

@ -0,0 +1,151 @@
# The behavior of RuboCop can be controlled via the .rubocop.yml
# configuration file. It makes it possible to enable/disable
# certain cops (checks) and to alter their behavior if they accept
# any parameters. The file can be placed either in your home
# directory or in some project directory.
#
# RuboCop will start looking for the configuration file in the directory
# where the inspected file is and continue its way up to the root directory.
#
# See https://github.com/rubocop-hq/rubocop/blob/master/manual/configuration.md
AllCops:
TargetRubyVersion: 2.6
SuggestExtensions: false
NewCops: enable
Exclude:
- 'test/**/*'
- 'rack-protection/**/*'
- 'sinatra-contrib/**/*'
- vendor/bundle/**/*
Layout/ExtraSpacing:
AllowForAlignment: true
AllowBeforeTrailingComments: true
# Temporary disable cops because warnings are fixed
Style/SingleLineMethods:
Enabled: false
Style/MutableConstant:
Enabled: false
Lint/AmbiguousBlockAssociation:
Enabled: false
Style/CaseEquality:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/Documentation:
Enabled: false
Lint/IneffectiveAccessModifier:
Enabled: false
Lint/RescueException:
Enabled: false
Style/SpecialGlobalVars:
Enabled: false
Bundler/DuplicatedGem:
Enabled: false
Layout/HeredocIndentation:
Enabled: false
Style/FormatStringToken:
Enabled: false
Lint/UselessAccessModifier:
Enabled: false
Style/ClassVars:
Enabled: false
Lint/UselessAssignment:
Enabled: false
Style/EmptyLiteral:
Enabled: false
Layout/LineLength:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Lint/SuppressedException:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/BlockLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Style/AccessModifierDeclarations:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/EvalWithLocation:
Enabled: false
Lint/MissingSuper:
Enabled: false
Style/MissingRespondToMissing:
Enabled: false
Style/MixinUsage:
Enabled: false
Style/MultilineTernaryOperator:
Enabled: false
Style/StructInheritance:
Enabled: false
Style/SymbolProc:
Enabled: false
Style/IfUnlessModifier:
Enabled: false
Style/OptionalBooleanParameter:
Enabled: false
Style/DocumentDynamicEvalDefinition:
Enabled: false
Lint/ToEnumArguments:
Enabled: false
Naming/MethodParameterName:
Enabled: false
Naming/AccessorMethodName:
Enabled: false
Style/SlicingWithRange:
Enabled: false

48
Gemfile
View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Why use bundler?
# Well, not all development dependencies install on all rubies. Moreover, `gem
# install sinatra --development` doesn't work, as it will also try to install
@ -12,40 +14,40 @@ gemspec
gem 'rake'
rack_version = ENV['rack'].to_s
rack_version = nil if rack_version.empty? or rack_version == 'stable'
rack_version = {:github => 'rack/rack'} if rack_version == 'master'
rack_version = nil if rack_version.empty? || (rack_version == 'stable')
rack_version = { github: 'rack/rack' } if rack_version == 'master'
gem 'rack', rack_version
gem 'minitest', '~> 5.0'
gem 'rack-test', github: 'rack/rack-test'
gem "minitest", "~> 5.0"
gem 'rubocop', '~> 1.32.0', require: false
gem 'yard'
gem "rack-protection", path: "rack-protection"
gem "sinatra-contrib", path: "sinatra-contrib"
gem 'rack-protection', path: 'rack-protection'
gem 'sinatra-contrib', path: 'sinatra-contrib'
gem 'activesupport', '~> 6.1'
gem "activesupport", "~> 6.1"
gem 'redcarpet', platforms: [ :ruby ]
gem 'rdiscount', platforms: [ :ruby ]
gem 'puma'
gem 'falcon', '~> 0.40', platforms: [ :ruby ]
gem 'yajl-ruby', platforms: [ :ruby ]
gem 'nokogiri', '> 1.5.0'
gem 'rainbows', platforms: [ :mri ] # uses #fork
gem 'eventmachine'
gem 'slim', '~> 4'
gem 'rdoc'
gem 'kramdown'
gem 'markaby'
gem 'asciidoctor'
gem 'liquid'
gem 'rabl'
gem 'builder'
gem 'commonmarker', '~> 0.20.0', platforms: [:ruby]
gem 'erubi'
gem 'eventmachine'
gem 'falcon', '~> 0.40', platforms: [:ruby]
gem 'haml', '~> 5'
gem 'commonmarker', '~> 0.20.0', platforms: [ :ruby ]
gem 'kramdown'
gem 'liquid'
gem 'markaby'
gem 'nokogiri', '> 1.5.0'
gem 'pandoc-ruby', '~> 2.0.2'
gem 'puma'
gem 'rabl'
gem 'rainbows', platforms: [:mri] # uses #fork
gem 'rdiscount', platforms: [:ruby]
gem 'rdoc'
gem 'redcarpet', platforms: [:ruby]
gem 'simplecov', require: false
gem 'slim', '~> 4'
gem 'yajl-ruby', platforms: [:ruby]
gem 'json', platforms: [ :jruby, :mri ]
gem 'json', platforms: %i[jruby mri]

123
Rakefile
View File

@ -1,15 +1,17 @@
# frozen_string_literal: true
require 'rake/clean'
require 'rake/testtask'
require 'fileutils'
require 'date'
task :default => :test
task :spec => :test
task default: :test
task spec: :test
CLEAN.include "**/*.rbc"
CLEAN.include '**/*.rbc'
def source_version
@source_version ||= File.read(File.expand_path("VERSION", __dir__)).strip
@source_version ||= File.read(File.expand_path('VERSION', __dir__)).strip
end
def prev_feature
@ -17,7 +19,8 @@ def prev_feature
end
def prev_version
return prev_feature + '.0' if source_version.end_with? '.0'
return "#{prev_feature}.0" if source_version.end_with? '.0'
source_version.gsub(/\d+$/) { |s| s.to_i - 1 }
end
@ -29,13 +32,15 @@ Rake::TestTask.new(:test) do |t|
t.warning = true
end
Rake::TestTask.new(:"test:core") do |t|
core_tests = %w[base delegator encoding extensions filter
helpers mapped_error middleware rdoc
readme request response result route_added_hook
routing server settings sinatra static templates]
t.test_files = core_tests.map {|n| "test/#{n}_test.rb"}
t.ruby_opts = ["-r rubygems"] if defined? Gem
Rake::TestTask.new(:'test:core') do |t|
core_tests = %w[
base delegator encoding extensions filter
helpers mapped_error middleware rdoc
readme request response result route_added_hook
routing server settings sinatra static templates
]
t.test_files = core_tests.map { |n| "test/#{n}_test.rb" }
t.ruby_opts = ['-r rubygems'] if defined? Gem
t.warning = true
end
@ -44,7 +49,7 @@ end
namespace :test do
desc 'Measures test coverage'
task :coverage do
rm_f "coverage"
rm_f 'coverage'
ENV['COVERAGE'] = '1'
Rake::Task['test'].invoke
end
@ -53,26 +58,26 @@ end
# Website =============================================================
desc 'Generate RDoc under doc/api'
task 'doc' => ['doc:api']
task('doc:api') { sh "yardoc -o doc/api" }
task 'doc' => ['doc:api']
task('doc:api') { sh 'yardoc -o doc/api' }
CLEAN.include 'doc/api'
# README ===============================================================
task :add_template, [:name] do |t, args|
task :add_template, [:name] do |_t, args|
Dir.glob('README.*') do |file|
code = File.read(file)
if code =~ /^===.*#{args.name.capitalize}/
puts "Already covered in #{file}"
else
template = code[/===[^\n]*Liquid.*index\.liquid<\/tt>[^\n]*/m]
if !template
puts "Liquid not found in #{file}"
else
template = code[%r{===[^\n]*Liquid.*index\.liquid</tt>[^\n]*}m]
if template
puts "Adding section to #{file}"
template = template.gsub(/Liquid/, args.name.capitalize).gsub(/liquid/, args.name.downcase)
code.gsub! /^(\s*===.*CoffeeScript)/, "\n" << template << "\n\\1"
File.open(file, "w") { |f| f << code }
File.open(file, 'w') { |f| f << code }
else
puts "Liquid not found in #{file}"
end
end
end
@ -80,29 +85,31 @@ end
# Thanks in announcement ===============================================
team = ["Ryan Tomayko", "Blake Mizerany", "Simon Rozet", "Konstantin Haase", "Zachary Scott"]
desc "list of contributors"
task :thanks, ['release:all', :backports] do |t, a|
a.with_defaults :release => "#{prev_version}..HEAD",
:backports => "#{prev_feature}.0..#{prev_feature}.x"
team = ['Ryan Tomayko', 'Blake Mizerany', 'Simon Rozet', 'Konstantin Haase', 'Zachary Scott']
desc 'list of contributors'
task :thanks, ['release:all', :backports] do |_t, a|
a.with_defaults release: "#{prev_version}..HEAD",
backports: "#{prev_feature}.0..#{prev_feature}.x"
included = `git log --format=format:"%aN\t%s" #{a.release}`.lines.map { |l| l.force_encoding('binary') }
excluded = `git log --format=format:"%aN\t%s" #{a.backports}`.lines.map { |l| l.force_encoding('binary') }
commits = (included - excluded).group_by { |c| c[/^[^\t]+/] }
authors = commits.keys.sort_by { |n| - commits[n].size } - team
puts authors[0..-2].join(', ') << " and " << authors.last,
"(based on commits included in #{a.release}, but not in #{a.backports})"
puts authors[0..-2].join(', ') << ' and ' << authors.last,
"(based on commits included in #{a.release}, but not in #{a.backports})"
end
desc "list of authors"
task :authors, [:commit_range, :format, :sep] do |t, a|
a.with_defaults :format => "%s (%d)", :sep => ", ", :commit_range => '--all'
desc 'list of authors'
task :authors, [:commit_range, :format, :sep] do |_t, a|
a.with_defaults format: '%s (%d)', sep: ', ', commit_range: '--all'
authors = Hash.new(0)
blake = "Blake Mizerany"
blake = 'Blake Mizerany'
overall = 0
mapping = {
"blake.mizerany@gmail.com" => blake, "bmizerany" => blake,
"a_user@mac.com" => blake, "ichverstehe" => "Harry Vangberg",
"Wu Jiang (nouse)" => "Wu Jiang" }
'blake.mizerany@gmail.com' => blake, 'bmizerany' => blake,
'a_user@mac.com' => blake, 'ichverstehe' => 'Harry Vangberg',
'Wu Jiang (nouse)' => 'Wu Jiang'
}
`git shortlog -s #{a.commit_range}`.lines.map do |line|
line = line.force_encoding 'binary' if line.respond_to? :force_encoding
num, name = line.split("\t", 2).map(&:strip)
@ -110,18 +117,18 @@ task :authors, [:commit_range, :format, :sep] do |t, a|
overall += num.to_i
end
puts "#{overall} commits by #{authors.count} authors:"
puts authors.sort_by { |n,c| -c }.map { |e| a.format % e }.join(a.sep)
puts authors.sort_by { |_n, c| -c }.map { |e| a.format % e }.join(a.sep)
end
desc "generates TOC"
task :toc, [:readme] do |t, a|
a.with_defaults :readme => 'README.md'
desc 'generates TOC'
task :toc, [:readme] do |_t, a|
a.with_defaults readme: 'README.md'
def self.link(title)
title.downcase.gsub(/(?!-)\W /, '-').gsub(' ', '-').gsub(/(?!-)\W/, '')
end
puts "* [Sinatra](#sinatra)"
puts '* [Sinatra](#sinatra)'
title = Regexp.new('(?<=\* )(.*)') # so Ruby 1.8 doesn't complain
File.binread(a.readme).scan(/^##.*/) do |line|
puts line.gsub(/#(?=#)/, ' ').gsub('#', '*').gsub(title) { "[#{$1}](##{link($1)})" }
@ -132,12 +139,12 @@ end
if defined?(Gem)
GEMS_AND_ROOT_DIRECTORIES = {
"sinatra" => ".",
"sinatra-contrib" => "./sinatra-contrib",
"rack-protection" => "./rack-protection"
}
'sinatra' => '.',
'sinatra-contrib' => './sinatra-contrib',
'rack-protection' => './rack-protection'
}.freeze
def package(gem, ext='')
def package(gem, ext = '')
"pkg/#{gem}-#{source_version}" + ext
end
@ -145,12 +152,12 @@ if defined?(Gem)
CLOBBER.include('pkg')
GEMS_AND_ROOT_DIRECTORIES.each do |gem, directory|
file package(gem, '.gem') => ["pkg/", "#{directory + '/' + gem}.gemspec"] do |f|
file package(gem, '.gem') => ['pkg/', "#{"#{directory}/#{gem}"}.gemspec"] do |f|
sh "cd #{directory} && gem build #{gem}.gemspec"
mv directory + "/" + File.basename(f.name), f.name
mv "#{directory}/#{File.basename(f.name)}", f.name
end
file package(gem, '.tar.gz') => ["pkg/"] do |f|
file package(gem, '.tar.gz') => ['pkg/'] do |f|
sh <<-SH
git archive \
--prefix=#{gem}-#{source_version}/ \
@ -161,29 +168,29 @@ if defined?(Gem)
end
namespace :package do
GEMS_AND_ROOT_DIRECTORIES.each do |gem, directory|
GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory|
desc "Build #{gem} packages"
task gem => %w[.gem .tar.gz].map { |e| package(gem, e) }
end
desc "Build all packages"
task :all => GEMS_AND_ROOT_DIRECTORIES.keys
desc 'Build all packages'
task all: GEMS_AND_ROOT_DIRECTORIES.keys
end
namespace :install do
GEMS_AND_ROOT_DIRECTORIES.each do |gem, directory|
GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory|
desc "Build and install #{gem} as local gem"
task gem => package(gem, '.gem') do
sh "gem install #{package(gem, '.gem')}"
end
end
desc "Build and install all of the gems as local gems"
task :all => GEMS_AND_ROOT_DIRECTORIES.keys
desc 'Build and install all of the gems as local gems'
task all: GEMS_AND_ROOT_DIRECTORIES.keys
end
namespace :release do
GEMS_AND_ROOT_DIRECTORIES.each do |gem, directory|
GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory|
desc "Release #{gem} as a package"
task gem => "package:#{gem}" do
sh <<-SH
@ -193,7 +200,7 @@ if defined?(Gem)
end
end
desc "Commits the version to github repository"
desc 'Commits the version to github repository'
task :commit_version do
%w[
lib/sinatra
@ -212,7 +219,7 @@ if defined?(Gem)
SH
end
desc "Release all gems as packages"
task :all => [:test, :commit_version] + GEMS_AND_ROOT_DIRECTORIES.keys
desc 'Release all gems as packages'
task all: %i[test commit_version] + GEMS_AND_ROOT_DIRECTORIES.keys
end
end

View File

@ -1,16 +1,18 @@
#!/usr/bin/env ruby -I ../lib -I lib
# coding: utf-8
# frozen_string_literal: true
require_relative 'rainbows'
require 'sinatra'
set :server, :rainbows
connections = []
get '/' do
halt erb(:login) unless params[:user]
erb :chat, :locals => { :user => params[:user].gsub(/\W/, '') }
erb :chat, locals: { user: params[:user].gsub(/\W/, '') }
end
get '/stream', :provides => 'text/event-stream' do
get '/stream', provides: 'text/event-stream' do
stream :keep_open do |out|
connections << out
out.callback { connections.delete(out) }

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rainbows'
module Rack
@ -8,7 +10,7 @@ module Rack
listeners: ["#{options[:Host]}:#{options[:Port]}"],
worker_processes: 1,
timeout: 30,
config_file: ::File.expand_path('rainbows.conf', __dir__),
config_file: ::File.expand_path('rainbows.conf', __dir__)
}
::Rainbows::HttpServer.new(app, rainbows_options).start.join

View File

@ -1,3 +1,5 @@
#!/usr/bin/env ruby -I ../lib -I lib
# frozen_string_literal: true
require 'sinatra'
get('/') { 'this is a simple app' }

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# this example does *not* work properly with WEBrick
#
# run *one* of these:

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/main'
enable :inline_templates

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,7 @@ module Sinatra
super(convert_key(key), convert_value(value))
end
alias_method :store, :[]=
alias store []=
def key(value)
super(convert_value(value))
@ -89,20 +89,21 @@ module Sinatra
super(convert_key(key))
end
alias_method :has_key?, :key?
alias_method :include?, :key?
alias_method :member?, :key?
alias has_key? key?
alias include? key?
alias member? key?
def value?(value)
super(convert_value(value))
end
alias_method :has_value?, :value?
alias has_value? value?
def delete(key)
super(convert_key(key))
end
# Added in Ruby 2.3
def dig(key, *other_keys)
super(convert_key(key), *other_keys)
end
@ -141,7 +142,7 @@ module Sinatra
self
end
alias_method :update, :merge!
alias update merge!
def merge(*other_hashes, &block)
dup.merge!(*other_hashes, &block)
@ -171,17 +172,19 @@ module Sinatra
def select(*args, &block)
return to_enum(:select) unless block_given?
dup.tap { |hash| hash.select!(*args, &block) }
end
def reject(*args, &block)
return to_enum(:reject) unless block_given?
dup.tap { |hash| hash.reject!(*args, &block) }
end
def compact
dup.tap(&:compact!)
end if method_defined?(:compact) # Added in Ruby 2.4
end
private

View File

@ -1,47 +1,49 @@
# frozen_string_literal: true
module Sinatra
ParamsConfig = {}
PARAMS_CONFIG = {}
if ARGV.any?
require 'optparse'
parser = OptionParser.new { |op|
op.on('-p port', 'set the port (default is 4567)') { |val| ParamsConfig[:port] = Integer(val) }
op.on('-s server', 'specify rack server/handler') { |val| ParamsConfig[:server] = val }
op.on('-q', 'turn on quiet mode (default is off)') { ParamsConfig[:quiet] = true }
op.on('-x', 'turn on the mutex lock (default is off)') { ParamsConfig[:lock] = true }
parser = OptionParser.new do |op|
op.on('-p port', 'set the port (default is 4567)') { |val| PARAMS_CONFIG[:port] = Integer(val) }
op.on('-s server', 'specify rack server/handler') { |val| PARAMS_CONFIG[:server] = val }
op.on('-q', 'turn on quiet mode (default is off)') { PARAMS_CONFIG[:quiet] = true }
op.on('-x', 'turn on the mutex lock (default is off)') { PARAMS_CONFIG[:lock] = true }
op.on('-e env', 'set the environment (default is development)') do |val|
ENV['RACK_ENV'] = val
ParamsConfig[:environment] = val.to_sym
PARAMS_CONFIG[:environment] = val.to_sym
end
op.on('-o addr', "set the host (default is (env == 'development' ? 'localhost' : '0.0.0.0'))") do |val|
ParamsConfig[:bind] = val
PARAMS_CONFIG[:bind] = val
end
}
end
begin
parser.parse!(ARGV.dup)
rescue => evar
ParamsConfig[:optparse_error] = evar
rescue StandardError => e
PARAMS_CONFIG[:optparse_error] = e
end
end
require 'sinatra/base'
class Application < Base
# we assume that the first file that requires 'sinatra' is the
# app_file. all other path related options are calculated based
# on this path by default.
set :app_file, caller_files.first || $0
set :run, Proc.new { File.expand_path($0) == File.expand_path(app_file) }
set :run, proc { File.expand_path($0) == File.expand_path(app_file) }
if run? && ARGV.any?
error = ParamsConfig.delete(:optparse_error)
error = PARAMS_CONFIG.delete(:optparse_error)
raise error if error
ParamsConfig.each { |k, v| set k, v }
PARAMS_CONFIG.each { |k, v| set k, v }
end
end
remove_const(:ParamsConfig)
remove_const(:PARAMS_CONFIG)
at_exit { Application.run! if $!.nil? && Application.run? }
end

View File

@ -12,6 +12,7 @@ module Sinatra
class ShowExceptions < Rack::ShowExceptions
@@eats_errors = Object.new
def @@eats_errors.flush(*) end
def @@eats_errors.puts(*) end
def initialize(app)
@ -21,23 +22,24 @@ module Sinatra
def call(env)
@app.call(env)
rescue Exception => e
errors, env["rack.errors"] = env["rack.errors"], @@eats_errors
errors = env['rack.errors']
env['rack.errors'] = @@eats_errors
if prefers_plain_text?(env)
content_type = "text/plain"
content_type = 'text/plain'
body = dump_exception(e)
else
content_type = "text/html"
content_type = 'text/html'
body = pretty(env, e)
end
env["rack.errors"] = errors
env['rack.errors'] = errors
[
500,
{
"Content-Type" => content_type,
"Content-Length" => body.bytesize.to_s
'Content-Type' => content_type,
'Content-Length' => body.bytesize.to_s
},
[body]
]
@ -49,27 +51,27 @@ module Sinatra
private
def bad_request?(e)
Sinatra::BadRequest === e
def bad_request?(exception)
Sinatra::BadRequest === exception
end
def prefers_plain_text?(env)
!(Request.new(env).preferred_type("text/plain","text/html") == "text/html") &&
[/curl/].index { |item| item =~ env["HTTP_USER_AGENT"] }
Request.new(env).preferred_type('text/plain', 'text/html') != 'text/html' &&
[/curl/].index { |item| item =~ env['HTTP_USER_AGENT'] }
end
def frame_class(frame)
if frame.filename =~ %r{lib/sinatra.*\.rb}
"framework"
'framework'
elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) ||
frame.filename =~ %r{/bin/(\w+)\z}
"system"
'system'
else
"app"
'app'
end
end
TEMPLATE = ERB.new <<-HTML # :nodoc:
TEMPLATE = ERB.new <<-HTML # :nodoc:
<!DOCTYPE html>
<html>
<head>
@ -357,6 +359,6 @@ enabled the <code>show_exceptions</code> setting.</p>
</div> <!-- /WRAP -->
</body>
</html>
HTML
HTML
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Sinatra
VERSION = '3.0.0'
end

View File

@ -1,11 +1,13 @@
source "https://rubygems.org"
# frozen_string_literal: true
source 'https://rubygems.org'
# encoding: utf-8
gem 'rake'
rack_version = ENV['rack'].to_s
rack_version = nil if rack_version.empty? or rack_version == 'stable'
rack_version = {:github => 'rack/rack'} if rack_version == 'master'
rack_version = nil if rack_version.empty? || (rack_version == 'stable')
rack_version = { github: 'rack/rack' } if rack_version == 'master'
gem 'rack', rack_version
gem 'sinatra', path: '..'

View File

@ -1,14 +1,15 @@
# encoding: utf-8
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path('lib', __dir__)
begin
require 'bundler'
Bundler::GemHelper.install_tasks
rescue LoadError => e
$stderr.puts e
warn e
end
desc "run specs"
desc 'run specs'
task(:spec) { ruby '-S rspec' }
namespace :doc do
@ -16,38 +17,39 @@ namespace :doc do
Dir.glob 'lib/rack/protection/*.rb' do |file|
excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
next if excluded_files.include?(file)
doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
Dir.mkdir "doc" unless File.directory? "doc"
file = "doc/#{file[4..-4].tr('/_', '-')}.rdoc"
Dir.mkdir 'doc' unless File.directory? 'doc'
puts "writing #{file}"
File.open(file, "w") { |f| f << doc }
File.open(file, 'w') { |f| f << doc }
end
end
task :index do
doc = File.read("README.md")
file = "doc/rack-protection-readme.md"
Dir.mkdir "doc" unless File.directory? "doc"
doc = File.read('README.md')
file = 'doc/rack-protection-readme.md'
Dir.mkdir 'doc' unless File.directory? 'doc'
puts "writing #{file}"
File.open(file, "w") { |f| f << doc }
File.open(file, 'w') { |f| f << doc }
end
task :all => [:readmes, :index]
task all: %i[readmes index]
end
desc "generate documentation"
task :doc => 'doc:all'
desc 'generate documentation'
task doc: 'doc:all'
desc "generate gemspec"
desc 'generate gemspec'
task 'rack-protection.gemspec' do
require 'rack/protection/version'
content = File.binread 'rack-protection.gemspec'
# fetch data
fields = {
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
:email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
:files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
authors: `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
email: ['mail@zzak.io', 'konstantin.haase@gmail.com'],
files: %w[License README.md Rakefile Gemfile rack-protection.gemspec] + Dir['lib/**/*']
}
# insert data
@ -67,6 +69,6 @@ task 'rack-protection.gemspec' do
File.open('rack-protection.gemspec', 'w') { |f| f << content }
end
task :gemspec => 'rack-protection.gemspec'
task :default => :spec
task :test => :spec
task gemspec: 'rack-protection.gemspec'
task default: :spec
task test: :spec

View File

@ -1 +0,0 @@
require "rack/protection"

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection/version'
require 'rack'
@ -29,7 +31,7 @@ module Rack
use_these = Array options[:use]
if options.fetch(:without_session, false)
except += [:session_hijacking, :remote_token]
except += %i[session_hijacking remote_token]
end
Rack::Builder.new do

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
require 'securerandom'
require 'openssl'
@ -19,7 +21,7 @@ module Rack
#
# It is not OOTB-compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem.
# For that, the following patch needs to be applied:
#
#
# Rack::Protection::AuthenticityToken.default_options(key: "csrf.token", authenticity_param: "_csrf")
#
# == Options
@ -95,12 +97,12 @@ module Rack
class AuthenticityToken < Base
TOKEN_LENGTH = 32
default_options :authenticity_param => 'authenticity_token',
:key => :csrf,
:allow_if => nil
default_options authenticity_param: 'authenticity_token',
key: :csrf,
allow_if: nil
def self.token(session, path: nil, method: :post)
self.new(nil).mask_authenticity_token(session, path: path, method: method)
new(nil).mask_authenticity_token(session, path: path, method: method)
end
def self.random_token
@ -114,8 +116,8 @@ module Rack
safe?(env) ||
valid_token?(env, env['HTTP_X_CSRF_TOKEN']) ||
valid_token?(env, Request.new(env).params[options[:authenticity_param]]) ||
( options[:allow_if] && options[:allow_if].call(env) )
rescue
options[:allow_if]&.call(env)
rescue StandardError
false
end
@ -123,10 +125,10 @@ module Rack
set_token(session)
token = if path && method
per_form_token(session, path, method)
else
global_token(session)
end
per_form_token(session, path, method)
else
global_token(session)
end
mask_token(token)
end
@ -185,7 +187,7 @@ module Rack
# value and decrypt it
token_length = masked_token.length / 2
one_time_pad = masked_token[0...token_length]
encrypted_token = masked_token[token_length..-1]
encrypted_token = masked_token[token_length..]
xor_byte_strings(one_time_pad, encrypted_token)
end
@ -207,8 +209,7 @@ module Rack
def compare_with_per_form_token(token, session, request)
secure_compare(token,
per_form_token(session, request.path.chomp('/'), request.request_method)
)
per_form_token(session, request.path.chomp('/'), request.request_method))
end
def real_token(session)
@ -233,7 +234,7 @@ module Rack
def token_hmac(session, identifier)
OpenSSL::HMAC.digest(
OpenSSL::Digest::SHA256.new,
OpenSSL::Digest.new('SHA256'),
real_token(session),
identifier
)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
require 'rack/utils'
require 'digest'
@ -8,12 +10,12 @@ module Rack
module Protection
class Base
DEFAULT_OPTIONS = {
:reaction => :default_reaction, :logging => true,
:message => 'Forbidden', :encryptor => Digest::SHA1,
:session_key => 'rack.session', :status => 403,
:allow_empty_referrer => true,
:report_key => "protection.failed",
:html_types => %w[text/html application/xhtml text/xml application/xml]
reaction: :default_reaction, logging: true,
message: 'Forbidden', encryptor: Digest::SHA1,
session_key: 'rack.session', status: 403,
allow_empty_referrer: true,
report_key: 'protection.failed',
html_types: %w[text/html application/xhtml text/xml application/xml]
}
attr_reader :app, :options
@ -31,7 +33,8 @@ module Rack
end
def initialize(app, options = {})
@app, @options = app, default_options.merge(options)
@app = app
@options = default_options.merge(options)
end
def safe?(env)
@ -52,24 +55,26 @@ module Rack
def react(env)
result = send(options[:reaction], env)
result if Array === result and result.size == 3
result if (Array === result) && (result.size == 3)
end
def warn(env, message)
return unless options[:logging]
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
l.warn(message)
end
def instrument(env)
return unless i = options[:instrumenter]
return unless (i = options[:instrumenter])
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
i.instrument('rack.protection', env)
end
def deny(env)
warn env, "attack prevented by #{self.class}"
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
[options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]]
end
def report(env)
@ -83,7 +88,8 @@ module Rack
def session(env)
return env[options[:session_key]] if session? env
fail "you need to set up a session middleware *before* #{self.class}"
raise "you need to set up a session middleware *before* #{self.class}"
end
def drop_session(env)
@ -92,7 +98,8 @@ module Rack
def referrer(env)
ref = env['HTTP_REFERER'].to_s
return if !options[:allow_empty_referrer] and ref.empty?
return if !options[:allow_empty_referrer] && ref.empty?
URI.parse(ref).host || Request.new(env).host
rescue URI::InvalidURIError
end
@ -102,7 +109,7 @@ module Rack
end
def random_string(secure = defined? SecureRandom)
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
rescue NotImplementedError
random_string false
end
@ -118,8 +125,9 @@ module Rack
alias default_reaction deny
def html?(headers)
return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
options[:html_types].include? header.last[/^\w+\/\w+/]
return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
options[:html_types].include? header.last[%r{^\w+/\w+}]
end
end
end

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -38,16 +39,16 @@ module Rack
class ContentSecurityPolicy < Base
default_options default_src: "'self'", report_only: false
DIRECTIVES = %i(base_uri child_src connect_src default_src
DIRECTIVES = %i[base_uri child_src connect_src default_src
font_src form_action frame_ancestors frame_src
img_src manifest_src media_src object_src
plugin_types referrer reflected_xss report_to
report_uri require_sri_for sandbox script_src
style_src worker_src webrtc_src navigate_to
prefetch_src).freeze
prefetch_src].freeze
NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
upgrade_insecure_requests).freeze
NO_ARG_DIRECTIVES = %i[block_all_mixed_content disown_opener
upgrade_insecure_requests].freeze
def csp_policy
directives = []

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
require 'pathname'
@ -29,9 +31,8 @@ module Rack
cookie_header = env['HTTP_COOKIE']
cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
cookies.each do |k, v|
if k == session_key && Array(v).size > 1
bad_cookies << k
elsif k != session_key && Rack::Utils.unescape(k) == session_key
if (k == session_key && Array(v).size > 1) ||
(k != session_key && Rack::Utils.unescape(k) == session_key)
bad_cookies << k
end
end
@ -40,6 +41,7 @@ module Rack
def remove_bad_cookies(request, response)
return if bad_cookies.empty?
paths = cookie_paths(request.path)
bad_cookies.each do |name|
paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
@ -49,7 +51,7 @@ module Rack
def redirect(env)
request = Request.new(env)
warn env, "attack prevented by #{self.class}"
[302, {'Content-Type' => 'text/html', 'Location' => request.path}, []]
[302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
end
def bad_cookies
@ -64,7 +66,7 @@ module Rack
end
def empty_cookie(host, path)
{:value => '', :domain => host, :path => path, :expires => Time.at(0)}
{ value: '', domain: host, path: path, expires: Time.at(0) }
end
def session_key

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'openssl'
require 'zlib'
require 'json'
@ -67,7 +69,7 @@ module Rack
end
def decode(str)
str.unpack('m').first
str.unpack1('m')
end
# Encode session cookies as Marshaled Base64 data
@ -78,7 +80,12 @@ module Rack
def decode(str)
return unless str
::Marshal.load(super(str)) rescue nil
begin
::Marshal.load(super(str))
rescue StandardError
nil
end
end
end
@ -91,7 +98,12 @@ module Rack
def decode(str)
return unless str
::JSON.parse(super(str)) rescue nil
begin
::JSON.parse(super(str))
rescue StandardError
nil
end
end
end
@ -102,8 +114,9 @@ module Rack
def decode(str)
return unless str
::JSON.parse(Zlib::Inflate.inflate(super(str)))
rescue
rescue StandardError
nil
end
end
@ -127,12 +140,12 @@ module Rack
attr_reader :coder
def initialize(app, options={})
def initialize(app, options = {})
# Assume keys are hex strings and convert them to raw byte strings for
# actual key material
@secrets = options.values_at(:secret, :old_secret).compact.map { |secret|
@secrets = options.values_at(:secret, :old_secret).compact.map do |secret|
[secret].pack('H*')
}
end
warn <<-MSG unless secure?(options)
SECURITY WARNING: No secret option provided to Rack::Protection::EncryptedCookie.
@ -154,7 +167,7 @@ module Rack
Called from: #{caller[0]}.
MSG
if options.has_key?(:legacy_hmac_secret)
if options.key?(:legacy_hmac_secret)
@legacy_hmac = options.fetch(:legacy_hmac, OpenSSL::Digest::SHA1)
# Multiply the :digest_length: by 2 because this value is the length of
@ -173,19 +186,19 @@ module Rack
# If no encryption is used, rely on the previous default (Base64::Marshal)
@coder = (options[:coder] ||= (@secrets.any? ? Marshal.new : Base64::Marshal.new))
super(app, options.merge!(:cookie_only => true))
super(app, options.merge!(cookie_only: true))
end
private
def find_session(req, sid)
def find_session(req, _sid)
data = unpacked_cookie_data(req)
data = persistent_session_id!(data)
[data["session_id"], data]
[data['session_id'], data]
end
def extract_session_id(request)
unpacked_cookie_data(request)["session_id"]
unpacked_cookie_data(request)['session_id']
end
def unpacked_cookie_data(request)
@ -214,14 +227,14 @@ module Rack
end
end
def persistent_session_id!(data, sid=nil)
def persistent_session_id!(data, sid = nil)
data ||= {}
data["session_id"] ||= sid || generate_sid
data['session_id'] ||= sid || generate_sid
data
end
def write_session(req, session_id, session, options)
session = session.merge("session_id" => session_id)
def write_session(req, session_id, session, _options)
session = session.merge('session_id' => session_id)
session_data = coder.encode(session)
unless @secrets.empty?
@ -229,14 +242,14 @@ module Rack
end
if session_data.size > (4096 - @key.size)
req.get_header(RACK_ERRORS).puts("Warning! Rack::Protection::EncryptedCookie data size exceeds 4K.")
req.get_header(RACK_ERRORS).puts('Warning! Rack::Protection::EncryptedCookie data size exceeds 4K.')
nil
else
session_data
end
end
def delete_session(req, session_id, options)
def delete_session(_req, _session_id, options)
# Nothing to do here, data is in the client
generate_sid unless options[:drop]
end
@ -253,7 +266,7 @@ module Rack
def secure?(options)
@secrets.size >= 1 ||
(options[:coder] && options[:let_coder_handle_secure_encoding])
(options[:coder] && options[:let_coder_handle_secure_encoding])
end
end
end

View File

@ -1,21 +1,23 @@
# frozen_string_literal: true
require 'openssl'
module Rack
module Protection
module Encryptor
CIPHER = 'aes-256-gcm'.freeze
DELIMITER = '--'.freeze
CIPHER = 'aes-256-gcm'
DELIMITER = '--'
def self.base64_encode(str)
[str].pack('m0')
end
def self.base64_decode(str)
str.unpack('m0').first
str.unpack1('m0')
end
def self.encrypt_message(data, secret, auth_data = '')
raise ArgumentError, "data cannot be nil" if data.nil?
raise ArgumentError, 'data cannot be nil' if data.nil?
cipher = OpenSSL::Cipher.new(CIPHER)
cipher.encrypt
@ -52,7 +54,6 @@ module Rack
decrypted_data = cipher.update(cipher_text)
decrypted_data << cipher.final
decrypted_data
rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
nil
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
require 'rack/utils'
require 'tempfile'
@ -29,8 +31,8 @@ module Rack
public :escape_html
end
default_options :escape => :html,
:escaper => defined?(EscapeUtils) ? EscapeUtils : self
default_options escape: :html,
escaper: defined?(EscapeUtils) ? EscapeUtils : self
def initialize(*)
super
@ -41,15 +43,19 @@ module Rack
@javascript = modes.include? :javascript
@url = modes.include? :url
if @javascript and not @escaper.respond_to? :escape_javascript
fail("Use EscapeUtils for JavaScript escaping.")
end
return unless @javascript && (!@escaper.respond_to? :escape_javascript)
raise('Use EscapeUtils for JavaScript escaping.')
end
def call(env)
request = Request.new(env)
get_was = handle(request.GET)
post_was = handle(request.POST) rescue nil
post_was = begin
handle(request.POST)
rescue StandardError
nil
end
app.call env
ensure
request.GET.replace get_was if get_was
@ -68,13 +74,12 @@ module Rack
when Array then object.map { |o| escape(o) }
when String then escape_string(object)
when Tempfile then object
else nil
end
end
def escape_hash(hash)
hash = hash.dup
hash.each { |k,v| hash[k] = escape(v) }
hash.each { |k, v| hash[k] = escape(v) }
hash
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -16,7 +18,7 @@ module Rack
# Compatible with rack-csrf.
class FormToken < AuthenticityToken
def accepts?(env)
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" or super
env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' or super
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -17,7 +19,7 @@ module Rack
# frame. Use :deny to forbid any embedding, :sameorigin
# to allow embedding from the same origin (default).
class FrameOptions < Base
default_options :frame_options => :sameorigin
default_options frame_options: :sameorigin
def frame_options
@frame_options ||= begin

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -19,7 +21,7 @@ module Rack
class HttpOrigin < Base
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
default_reaction :deny
default_options :allow_if => nil
default_options allow_if: nil
def base_url(env)
request = Rack::Request.new(env)
@ -29,14 +31,13 @@ module Rack
def accepts?(env)
return true if safe? env
return true unless origin = env['HTTP_ORIGIN']
return true unless (origin = env['HTTP_ORIGIN'])
return true if base_url(env) == origin
return true if options[:allow_if] && options[:allow_if].call(env)
return true if options[:allow_if]&.call(env)
permitted_origins = options[:permitted_origins]
Array(permitted_origins).include? origin
end
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -13,9 +15,11 @@ module Rack
def accepts?(env)
return true unless env.include? 'HTTP_X_FORWARDED_FOR'
ips = env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)
return false if env.include? 'HTTP_CLIENT_IP' and not ips.include? env['HTTP_CLIENT_IP']
return false if env.include? 'HTTP_X_REAL_IP' and not ips.include? env['HTTP_X_REAL_IP']
return false if env.include?('HTTP_CLIENT_IP') && (!ips.include? env['HTTP_CLIENT_IP'])
return false if env.include?('HTTP_X_REAL_IP') && (!ips.include? env['HTTP_X_REAL_IP'])
true
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -17,7 +19,7 @@ module Rack
#
# The `:allow_if` option can be set to a proc to use custom allow/deny logic.
class JsonCsrf < Base
default_options :allow_if => nil
default_options allow_if: nil
alias react deny
@ -36,8 +38,9 @@ module Rack
def has_vector?(request, headers)
return false if request.xhr?
return false if options[:allow_if] && options[:allow_if].call(request.env)
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
return false if options[:allow_if]&.call(request.env)
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$}
origin(request.env).nil? and referrer(request.env) != request.host
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -11,11 +13,11 @@ module Rack
# Thus <tt>GET /foo/%2e%2e%2fbar</tt> becomes <tt>GET /bar</tt>.
class PathTraversal < Base
def call(env)
path_was = env["PATH_INFO"]
env["PATH_INFO"] = cleanup path_was if path_was && !path_was.empty?
path_was = env['PATH_INFO']
env['PATH_INFO'] = cleanup path_was if path_was && !path_was.empty?
app.call env
ensure
env["PATH_INFO"] = path_was
env['PATH_INFO'] = path_was
end
def cleanup(path)
@ -29,12 +31,13 @@ module Rack
unescaped = unescaped.gsub(backslash, slash)
unescaped.split(slash).each do |part|
next if part.empty? or part == dot
next if part.empty? || (part == dot)
part == '..' ? parts.pop : parts << part
end
cleaned = slash + parts.join(slash)
cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
cleaned << slash if parts.any? && unescaped =~ (%r{/\.{0,2}$})
cleaned
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -13,7 +15,7 @@ module Rack
# Options:
# referrer_policy:: The policy to use (default: 'strict-origin-when-cross-origin')
class ReferrerPolicy < Base
default_options :referrer_policy => 'strict-origin-when-cross-origin'
default_options referrer_policy: 'strict-origin-when-cross-origin'
def call(env)
status, headers, body = @app.call(env)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -13,14 +15,14 @@ module Rack
# spoofed, too, this will not prevent determined hijacking attempts.
class SessionHijacking < Base
default_reaction :drop_session
default_options :tracking_key => :tracking,
:track => %w[HTTP_USER_AGENT]
default_options tracking_key: :tracking,
track: %w[HTTP_USER_AGENT]
def accepts?(env)
session = session env
key = options[:tracking_key]
if session.include? key
session[key].all? { |k,v| v == encode(env[k]) }
session[key].all? { |k, v| v == encode(env[k]) }
else
session[key] = {}
options[:track].each { |k| session[key][k] = encode(env[k]) }

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -18,11 +20,11 @@ module Rack
# preload:: Allow this domain to be included in browsers HSTS preload list. See https://hstspreload.appspot.com/
class StrictTransport < Base
default_options :max_age => 31_536_000, :include_subdomains => false, :preload => false
default_options max_age: 31_536_000, include_subdomains: false, preload: false
def strict_transport
@strict_transport ||= begin
strict_transport = 'max-age=' + options[:max_age].to_s
strict_transport = "max-age=#{options[:max_age]}"
strict_transport += '; includeSubDomains' if options[:include_subdomains]
strict_transport += '; preload' if options[:preload]
strict_transport.to_str

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Rack
module Protection
VERSION = '3.0.0'

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rack/protection'
module Rack
@ -12,7 +14,7 @@ module Rack
# Options:
# xss_mode:: How the browser should prevent the attack (default: :block)
class XSSHeader < Base
default_options :xss_mode => :block, :nosniff => true
default_options xss_mode: :block, nosniff: true
def call(env)
status, headers, body = @app.call(env)

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
require 'rack/protection'

View File

@ -1,40 +1,45 @@
version = File.read(File.expand_path("../VERSION", __dir__)).strip
# frozen_string_literal: true
version = File.read(File.expand_path('../VERSION', __dir__)).strip
Gem::Specification.new do |s|
# general infos
s.name = "rack-protection"
s.name = 'rack-protection'
s.version = version
s.description = "Protect against typical web attacks, works with all Rack apps, including Rails."
s.homepage = "http://sinatrarb.com/protection/"
s.description = 'Protect against typical web attacks, works with all Rack apps, including Rails.'
s.homepage = 'http://sinatrarb.com/protection/'
s.summary = s.description
s.license = 'MIT'
s.authors = ["https://github.com/sinatra/sinatra/graphs/contributors"]
s.email = "sinatrarb@googlegroups.com"
s.files = Dir["lib/**/*.rb"] + [
"License",
"README.md",
"Rakefile",
"Gemfile",
"rack-protection.gemspec"
s.authors = ['https://github.com/sinatra/sinatra/graphs/contributors']
s.email = 'sinatrarb@googlegroups.com'
s.files = Dir['lib/**/*.rb'] + [
'License',
'README.md',
'Rakefile',
'Gemfile',
'rack-protection.gemspec'
]
if s.respond_to?(:metadata)
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/rack-protection',
'homepage_uri' => 'http://sinatrarb.com/protection/',
'documentation_uri' => 'https://www.rubydoc.info/gems/rack-protection'
}
else
raise <<-EOF
unless s.respond_to?(:metadata)
raise <<-WARN
RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running:
gem install rubygems-update
update_rubygems:
gem update --system
EOF
WARN
end
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/rack-protection',
'homepage_uri' => 'http://sinatrarb.com/protection/',
'documentation_uri' => 'https://www.rubydoc.info/gems/rack-protection',
'rubygems_mfa_required' => 'true'
}
s.required_ruby_version = '>= 2.6.0'
# dependencies
s.add_dependency "rack"
s.add_development_dependency "rack-test", "~> 2"
s.add_development_dependency "rspec", "~> 3"
s.add_dependency 'rack'
s.add_development_dependency 'rack-test', '~> 2'
s.add_development_dependency 'rspec', '~> 3'
end

View File

@ -1,68 +1,70 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::AuthenticityToken do
let(:token) { described_class.random_token }
let(:masked_token) { described_class.token(session) }
let(:bad_token) { Base64.strict_encode64("badtoken") }
let(:session) { {:csrf => token} }
let(:bad_token) { Base64.strict_encode64('badtoken') }
let(:session) { { csrf: token } }
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it "denies post requests without any token" do
it 'denies post requests without any token' do
expect(post('/')).not_to be_ok
end
it "accepts post requests with correct X-CSRF-Token header" do
it 'accepts post requests with correct X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end
it "accepts post requests with masked X-CSRF-Token header" do
it 'accepts post requests with masked X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token)
expect(last_response).to be_ok
end
it "denies post requests with wrong X-CSRF-Token header" do
it 'denies post requests with wrong X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end
it "accepts post form requests with correct authenticity_token field" do
post('/', {"authenticity_token" => token}, 'rack.session' => session)
it 'accepts post form requests with correct authenticity_token field' do
post('/', { 'authenticity_token' => token }, 'rack.session' => session)
expect(last_response).to be_ok
end
it "accepts post form requests with masked authenticity_token field" do
post('/', {"authenticity_token" => masked_token}, 'rack.session' => session)
it 'accepts post form requests with masked authenticity_token field' do
post('/', { 'authenticity_token' => masked_token }, 'rack.session' => session)
expect(last_response).to be_ok
end
it "denies post form requests with wrong authenticity_token field" do
post('/', {"authenticity_token" => bad_token}, 'rack.session' => session)
it 'denies post form requests with wrong authenticity_token field' do
post('/', { 'authenticity_token' => bad_token }, 'rack.session' => session)
expect(last_response).not_to be_ok
end
it "accepts post form requests with a valid per form token" do
it 'accepts post form requests with a valid per form token' do
token = Rack::Protection::AuthenticityToken.token(session, path: '/foo')
post('/foo', {"authenticity_token" => token}, 'rack.session' => session)
post('/foo', { 'authenticity_token' => token }, 'rack.session' => session)
expect(last_response).to be_ok
end
it "denies post form requests with an invalid per form token" do
it 'denies post form requests with an invalid per form token' do
token = Rack::Protection::AuthenticityToken.token(session, path: '/foo')
post('/bar', {"authenticity_token" => token}, 'rack.session' => session)
post('/bar', { 'authenticity_token' => token }, 'rack.session' => session)
expect(last_response).not_to be_ok
end
it "prevents ajax requests without a valid token" do
expect(post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")).not_to be_ok
it 'prevents ajax requests without a valid token' do
expect(post('/', {}, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).not_to be_ok
end
it "allows for a custom authenticity token param" do
it 'allows for a custom authenticity token param' do
mock_app do
use Rack::Protection::AuthenticityToken, :authenticity_param => 'csrf_param'
run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
use Rack::Protection::AuthenticityToken, authenticity_param: 'csrf_param'
run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] }
end
post('/', {"csrf_param" => token}, 'rack.session' => {:csrf => token})
post('/', { 'csrf_param' => token }, 'rack.session' => { csrf: token })
expect(last_response).to be_ok
end
@ -71,10 +73,10 @@ RSpec.describe Rack::Protection::AuthenticityToken do
expect(env['rack.session'][:csrf]).not_to be_nil
end
it "allows for a custom token session key" do
it 'allows for a custom token session key' do
mock_app do
use Rack::Session::Cookie, :key => 'rack.session'
use Rack::Protection::AuthenticityToken, :key => :_csrf
use Rack::Session::Cookie, key: 'rack.session'
use Rack::Protection::AuthenticityToken, key: :_csrf
run DummyApp
end
@ -82,12 +84,12 @@ RSpec.describe Rack::Protection::AuthenticityToken do
expect(env['rack.session'][:_csrf]).not_to be_nil
end
describe ".token" do
it "returns a unique masked version of the authenticity token" do
describe '.token' do
it 'returns a unique masked version of the authenticity token' do
expect(Rack::Protection::AuthenticityToken.token(session)).not_to eq(masked_token)
end
it "sets a session authenticity token if one does not exist" do
it 'sets a session authenticity token if one does not exist' do
session = {}
allow(Rack::Protection::AuthenticityToken).to receive(:random_token).and_return(token)
allow_any_instance_of(Rack::Protection::AuthenticityToken).to receive(:mask_token).and_return(masked_token)
@ -96,8 +98,8 @@ RSpec.describe Rack::Protection::AuthenticityToken do
end
end
describe ".random_token" do
it "generates a base64 encoded 32 character string" do
describe '.random_token' do
it 'generates a base64 encoded 32 character string' do
expect(Base64.urlsafe_decode64(token).length).to eq(32)
end
end

View File

@ -1,37 +1,38 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::Base do
subject { described_class.new(-> {}) }
subject { described_class.new(lambda {}) }
describe "#random_string" do
it "outputs a string of 32 characters" do
describe '#random_string' do
it 'outputs a string of 32 characters' do
expect(subject.random_string.length).to eq(32)
end
end
describe "#referrer" do
it "Reads referrer from Referer header" do
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/valid"}
expect(subject.referrer(env)).to eq("bar.com")
describe '#referrer' do
it 'Reads referrer from Referer header' do
env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => 'http://bar.com/valid' }
expect(subject.referrer(env)).to eq('bar.com')
end
it "Reads referrer from Host header when Referer header is relative" do
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "/valid"}
expect(subject.referrer(env)).to eq("foo.com")
it 'Reads referrer from Host header when Referer header is relative' do
env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => '/valid' }
expect(subject.referrer(env)).to eq('foo.com')
end
it "Reads referrer from Host header when Referer header is missing" do
env = {"HTTP_HOST" => "foo.com"}
expect(subject.referrer(env)).to eq("foo.com")
it 'Reads referrer from Host header when Referer header is missing' do
env = { 'HTTP_HOST' => 'foo.com' }
expect(subject.referrer(env)).to eq('foo.com')
end
it "Returns nil when Referer header is missing and allow_empty_referrer is false" do
env = {"HTTP_HOST" => "foo.com"}
it 'Returns nil when Referer header is missing and allow_empty_referrer is false' do
env = { 'HTTP_HOST' => 'foo.com' }
subject.options[:allow_empty_referrer] = false
expect(subject.referrer(env)).to be_nil
end
it "Returns nil when Referer header is invalid" do
env = {"HTTP_HOST" => "foo.com", "HTTP_REFERER" => "http://bar.com/bad|uri"}
it 'Returns nil when Referer header is invalid' do
env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => 'http://bar.com/bad|uri' }
expect(subject.referrer(env)).to be_nil
end
end

View File

@ -1,66 +1,68 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::ContentSecurityPolicy do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'should set the Content Security Policy' do
expect(
get('/', {}, 'wants' => 'text/html').headers["Content-Security-Policy"]
get('/', {}, 'wants' => 'text/html').headers['Content-Security-Policy']
).to eq("default-src 'self'")
end
it 'should not set the Content Security Policy for other content types' do
headers = get('/', {}, 'wants' => 'text/foo').headers
expect(headers["Content-Security-Policy"]).to be_nil
expect(headers["Content-Security-Policy-Report-Only"]).to be_nil
expect(headers['Content-Security-Policy']).to be_nil
expect(headers['Content-Security-Policy-Report-Only']).to be_nil
end
it 'should allow changing the protection settings' do
mock_app do
use Rack::Protection::ContentSecurityPolicy, :default_src => 'none', :script_src => 'https://cdn.mybank.net', :style_src => 'https://cdn.mybank.net', :img_src => 'https://cdn.mybank.net', :connect_src => 'https://api.mybank.com', :frame_src => 'self', :font_src => 'https://cdn.mybank.net', :object_src => 'https://cdn.mybank.net', :media_src => 'https://cdn.mybank.net', :report_uri => '/my_amazing_csp_report_parser', :sandbox => 'allow-scripts'
use Rack::Protection::ContentSecurityPolicy, default_src: 'none', script_src: 'https://cdn.mybank.net', style_src: 'https://cdn.mybank.net', img_src: 'https://cdn.mybank.net', connect_src: 'https://api.mybank.com', frame_src: 'self', font_src: 'https://cdn.mybank.net', object_src: 'https://cdn.mybank.net', media_src: 'https://cdn.mybank.net', report_uri: '/my_amazing_csp_report_parser', sandbox: 'allow-scripts'
run DummyApp
end
headers = get('/', {}, 'wants' => 'text/html').headers
expect(headers["Content-Security-Policy"]).to eq("connect-src https://api.mybank.com; default-src none; font-src https://cdn.mybank.net; frame-src self; img-src https://cdn.mybank.net; media-src https://cdn.mybank.net; object-src https://cdn.mybank.net; report-uri /my_amazing_csp_report_parser; sandbox allow-scripts; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net")
expect(headers["Content-Security-Policy-Report-Only"]).to be_nil
expect(headers['Content-Security-Policy']).to eq('connect-src https://api.mybank.com; default-src none; font-src https://cdn.mybank.net; frame-src self; img-src https://cdn.mybank.net; media-src https://cdn.mybank.net; object-src https://cdn.mybank.net; report-uri /my_amazing_csp_report_parser; sandbox allow-scripts; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net')
expect(headers['Content-Security-Policy-Report-Only']).to be_nil
end
it 'should allow setting CSP3 no arg directives' do
mock_app do
use Rack::Protection::ContentSecurityPolicy, :block_all_mixed_content => true, :disown_opener => true, :upgrade_insecure_requests => true
use Rack::Protection::ContentSecurityPolicy, block_all_mixed_content: true, disown_opener: true, upgrade_insecure_requests: true
run DummyApp
end
headers = get('/', {}, 'wants' => 'text/html').headers
expect(headers["Content-Security-Policy"]).to eq("block-all-mixed-content; default-src 'self'; disown-opener; upgrade-insecure-requests")
expect(headers['Content-Security-Policy']).to eq("block-all-mixed-content; default-src 'self'; disown-opener; upgrade-insecure-requests")
end
it 'should ignore CSP3 no arg directives unless they are set to true' do
mock_app do
use Rack::Protection::ContentSecurityPolicy, :block_all_mixed_content => false, :disown_opener => 'false', :upgrade_insecure_requests => 'foo'
use Rack::Protection::ContentSecurityPolicy, block_all_mixed_content: false, disown_opener: 'false', upgrade_insecure_requests: 'foo'
run DummyApp
end
headers = get('/', {}, 'wants' => 'text/html').headers
expect(headers["Content-Security-Policy"]).to eq("default-src 'self'")
expect(headers['Content-Security-Policy']).to eq("default-src 'self'")
end
it 'should allow changing report only' do
# I have no clue what other modes are available
mock_app do
use Rack::Protection::ContentSecurityPolicy, :report_uri => '/my_amazing_csp_report_parser', :report_only => true
use Rack::Protection::ContentSecurityPolicy, report_uri: '/my_amazing_csp_report_parser', report_only: true
run DummyApp
end
headers = get('/', {}, 'wants' => 'text/html').headers
expect(headers["Content-Security-Policy"]).to be_nil
expect(headers["Content-Security-Policy-Report-Only"]).to eq("default-src 'self'; report-uri /my_amazing_csp_report_parser")
expect(headers['Content-Security-Policy']).to be_nil
expect(headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; report-uri /my_amazing_csp_report_parser")
end
it 'should not override the header if already set' do
mock_app with_headers("Content-Security-Policy" => "default-src: none")
expect(get('/', {}, 'wants' => 'text/html').headers["Content-Security-Policy"]).to eq("default-src: none")
mock_app with_headers('Content-Security-Policy' => 'default-src: none')
expect(get('/', {}, 'wants' => 'text/html').headers['Content-Security-Policy']).to eq('default-src: none')
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::CookieTossing do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
context 'with default reaction' do
before(:each) do
@ -34,7 +36,7 @@ rack.%2573ession=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970
rack.session=; domain=example.org; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
rack.session=; domain=example.org; path=/some; expires=Thu, 01 Jan 1970 00:00:00 GMT
rack.session=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970 00:00:00 GMT
END
END
expect(last_response.headers['Set-Cookie']).to eq(expected_header)
end
end
@ -42,7 +44,7 @@ END
context 'with redirect reaction' do
before(:each) do
mock_app do
use Rack::Protection::CookieTossing, :reaction => :redirect
use Rack::Protection::CookieTossing, reaction: :redirect
run DummyApp
end
end
@ -63,7 +65,7 @@ END
context 'with custom session key' do
it 'denies requests with duplicate session cookies' do
mock_app do
use Rack::Protection::CookieTossing, :session_key => '_session'
use Rack::Protection::CookieTossing, session_key: '_session'
run DummyApp
end

View File

@ -1,77 +1,79 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::EncryptedCookie do
let(:incrementor) do
lambda do |env|
env["rack.session"]["counter"] ||= 0
env["rack.session"]["counter"] += 1
hash = env["rack.session"].dup
hash.delete("session_id")
env['rack.session']['counter'] ||= 0
env['rack.session']['counter'] += 1
hash = env['rack.session'].dup
hash.delete('session_id')
Rack::Response.new(hash.inspect).to_a
end
end
let(:session_id) do
lambda do |env|
Rack::Response.new(env["rack.session"].to_hash.inspect).to_a
Rack::Response.new(env['rack.session'].to_hash.inspect).to_a
end
end
let(:session_option) do
lambda do |opt|
lambda do |env|
Rack::Response.new(env["rack.session.options"][opt].inspect).to_a
Rack::Response.new(env['rack.session.options'][opt].inspect).to_a
end
end
end
let(:nothing) do
lambda do |env|
Rack::Response.new("Nothing").to_a
lambda do |_env|
Rack::Response.new('Nothing').to_a
end
end
let(:renewer) do
lambda do |env|
env["rack.session.options"][:renew] = true
Rack::Response.new("Nothing").to_a
env['rack.session.options'][:renew] = true
Rack::Response.new('Nothing').to_a
end
end
let(:only_session_id) do
lambda do |env|
Rack::Response.new(env["rack.session"]["session_id"].to_s).to_a
Rack::Response.new(env['rack.session']['session_id'].to_s).to_a
end
end
let(:bigcookie) do
lambda do |env|
env["rack.session"]["cookie"] = "big" * 3000
Rack::Response.new(env["rack.session"].inspect).to_a
env['rack.session']['cookie'] = 'big' * 3000
Rack::Response.new(env['rack.session'].inspect).to_a
end
end
let(:destroy_session) do
lambda do |env|
env["rack.session"].destroy
Rack::Response.new("Nothing").to_a
env['rack.session'].destroy
Rack::Response.new('Nothing').to_a
end
end
def response_for(options={})
def response_for(options = {})
request_options = options.fetch(:request, {})
cookie = if options[:cookie].is_a?(Rack::Response)
options[:cookie]["Set-Cookie"]
else
options[:cookie]
end
request_options["HTTP_COOKIE"] = cookie || ""
options[:cookie]['Set-Cookie']
else
options[:cookie]
end
request_options['HTTP_COOKIE'] = cookie || ''
app_with_cookie = Rack::Protection::EncryptedCookie.new(*options[:app])
app_with_cookie = Rack::Lint.new(app_with_cookie)
Rack::MockRequest.new(app_with_cookie).get("/", request_options)
Rack::MockRequest.new(app_with_cookie).get('/', request_options)
end
def random_cipher_secret
OpenSSL::Cipher.new('aes-256-gcm').random_key.unpack('H*').first
OpenSSL::Cipher.new('aes-256-gcm').random_key.unpack1('H*')
end
let(:secret) { random_cipher_secret }
@ -99,7 +101,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it 'uses base64 to decode' do
coder = Rack::Protection::EncryptedCookie::Base64.new
str = ['fuuuuu'].pack('m0')
expect(coder.decode(str)).to eq(str.unpack('m0').first)
expect(coder.decode(str)).to eq(str.unpack1('m0'))
end
it 'handles non-strict base64 encoding' do
@ -118,7 +120,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it 'marshals and base64 decodes' do
coder = Rack::Protection::EncryptedCookie::Base64::Marshal.new
str = [::Marshal.dump('fuuuuu')].pack('m0')
expect(coder.decode(str)).to eq(::Marshal.load(str.unpack('m0').first))
expect(coder.decode(str)).to eq(::Marshal.load(str.unpack1('m0')))
end
it 'rescues failures on decode' do
@ -137,7 +139,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it 'JSON and base64 decodes' do
coder = Rack::Protection::EncryptedCookie::Base64::JSON.new
str = [::JSON.dump(%w[fuuuuu])].pack('m0')
expect(coder.decode(str)).to eq(::JSON.parse(str.unpack('m0').first))
expect(coder.decode(str)).to eq(::JSON.parse(str.unpack1('m0')))
end
it 'rescues failures on decode' do
@ -169,7 +171,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
end
end
it "warns if no secret is given" do
it 'warns if no secret is given' do
Rack::Protection::EncryptedCookie.new(incrementor)
expect(warnings.first).to match(/no secret/i)
warnings.clear
@ -178,7 +180,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
end
it 'warns if secret is to short' do
Rack::Protection::EncryptedCookie.new(incrementor, secret: secret[0,16])
Rack::Protection::EncryptedCookie.new(incrementor, secret: secret[0, 16])
expect(warnings.first).to match(/secret is not long enough/i)
warnings.clear
Rack::Protection::EncryptedCookie.new(incrementor, secret: secret)
@ -192,38 +194,46 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(warnings).to be_empty
end
it "still warns if coder is not set" do
it 'still warns if coder is not set' do
Rack::Protection::EncryptedCookie.new(
incrementor,
let_coder_handle_secure_encoding: true)
let_coder_handle_secure_encoding: true
)
expect(warnings.first).to match(/no secret/i)
end
it 'uses a coder' do
identity = Class.new {
identity = Class.new do
attr_reader :calls
def initialize
@calls = []
end
def encode(str); @calls << :encode; str; end
def decode(str); @calls << :decode; str; end
}.new
def encode(str)
@calls << :encode
str
end
def decode(str)
@calls << :decode
str
end
end.new
response = response_for(app: [incrementor, { coder: identity }])
expect(response["Set-Cookie"]).to include("rack.session=")
expect(response['Set-Cookie']).to include('rack.session=')
expect(response.body).to eq('{"counter"=>1}')
expect(identity.calls).to eq([:decode, :encode])
expect(identity.calls).to eq(%i[decode encode])
end
it "creates a new cookie" do
it 'creates a new cookie' do
response = response_for(app: incrementor)
expect(response["Set-Cookie"]).to include("rack.session=")
expect(response['Set-Cookie']).to include('rack.session=')
expect(response.body).to eq('{"counter"=>1}')
end
it "loads from a cookie" do
it 'loads from a cookie' do
response = response_for(app: incrementor)
response = response_for(app: incrementor, cookie: response)
@ -233,58 +243,58 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>3}')
end
it "renew session id" do
it 'renew session id' do
response = response_for(app: incrementor)
cookie = response['Set-Cookie']
response = response_for(app: only_session_id, cookie: cookie)
cookie = response['Set-Cookie'] if response['Set-Cookie']
expect(response.body).to_not eq("")
expect(response.body).to_not eq('')
old_session_id = response.body
response = response_for(app: renewer, cookie: cookie)
cookie = response['Set-Cookie'] if response['Set-Cookie']
response = response_for(app: only_session_id, cookie: cookie)
expect(response.body).to_not eq("")
expect(response.body).to_not eq('')
expect(response.body).to_not eq(old_session_id)
end
it "destroys session" do
it 'destroys session' do
response = response_for(app: incrementor)
response = response_for(app: only_session_id, cookie: response)
expect(response.body).to_not eq("")
expect(response.body).to_not eq('')
old_session_id = response.body
response = response_for(app: destroy_session, cookie: response)
response = response_for(app: only_session_id, cookie: response)
expect(response.body).to_not eq("")
expect(response.body).to_not eq('')
expect(response.body).to_not eq(old_session_id)
end
it "survives broken cookies" do
it 'survives broken cookies' do
response = response_for(
app: incrementor,
cookie: "rack.session=blarghfasel"
cookie: 'rack.session=blarghfasel'
)
expect(response.body).to eq('{"counter"=>1}')
response = response_for(
app: [incrementor, { secret: secret }],
cookie: "rack.session="
cookie: 'rack.session='
)
expect(response.body).to eq('{"counter"=>1}')
end
it "barks on too big cookies" do
expect {
it 'barks on too big cookies' do
expect do
response_for(app: bigcookie, request: { fatal: true })
}.to raise_error Rack::MockRequest::FatalWarning
end.to raise_error Rack::MockRequest::FatalWarning
end
it "loads from a cookie with integrity hash" do
it 'loads from a cookie with integrity hash' do
app = [incrementor, { secret: secret }]
response = response_for(app: app)
@ -300,7 +310,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>1}')
end
it "loads from a cookie with accept-only integrity hash for graceful key rotation" do
it 'loads from a cookie with accept-only integrity hash for graceful key rotation' do
response = response_for(app: [incrementor, { secret: secret }])
new_secret = random_cipher_secret
@ -319,7 +329,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it 'loads from a legacy hmac cookie' do
legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
legacy_secret = 'test legacy secret'
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session)
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session)
legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly"
@ -328,7 +338,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>2}')
end
it "ignores tampered with session cookies" do
it 'ignores tampered with session cookies' do
app = [incrementor, { secret: secret }]
response = response_for(app: app)
expect(response.body).to eq('{"counter"=>1}')
@ -336,7 +346,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
response = response_for(app: app, cookie: response)
expect(response.body).to eq('{"counter"=>2}')
ctxt, iv, auth_tag = response["Set-Cookie"].split("--", 3)
ctxt, iv, auth_tag = response['Set-Cookie'].split('--', 3)
tampered_with_cookie = [ctxt, iv, auth_tag.reverse].join('--')
response = response_for(app: app, cookie: tampered_with_cookie)
@ -346,7 +356,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it 'ignores tampered with legacy hmac cookie' do
legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
legacy_secret = 'test legacy secret'
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session).reverse
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session).reverse
legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly"
@ -355,7 +365,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>1}')
end
it "supports either of secret or old_secret" do
it 'supports either of secret or old_secret' do
app = [incrementor, { secret: secret }]
response = response_for(app: app)
expect(response.body).to eq('{"counter"=>1}')
@ -371,7 +381,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>2}')
end
it "supports custom digest class for legacy hmac cookie" do
it 'supports custom digest class for legacy hmac cookie' do
legacy_hmac = OpenSSL::Digest::SHA256
legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' })
legacy_secret = 'test legacy secret'
@ -379,7 +389,8 @@ RSpec.describe Rack::Protection::EncryptedCookie do
legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly"
app = [incrementor, {
secret: secret, legacy_hmac_secret: legacy_secret, legacy_hmac: legacy_hmac }]
secret: secret, legacy_hmac_secret: legacy_secret, legacy_hmac: legacy_hmac
}]
response = response_for(app: app, cookie: legacy_cookie)
expect(response.body).to eq('{"counter"=>2}')
@ -388,7 +399,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to eq('{"counter"=>3}')
end
it "can handle Rack::Lint middleware" do
it 'can handle Rack::Lint middleware' do
response = response_for(app: incrementor)
lint = Rack::Lint.new(session_id)
@ -396,11 +407,12 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to_not be_nil
end
it "can handle middleware that inspects the env" do
it 'can handle middleware that inspects the env' do
class TestEnvInspector
def initialize(app)
@app = app
end
def call(env)
env.inspect
@app.call(env)
@ -414,7 +426,7 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to_not be_nil
end
it "returns the session id in the session hash" do
it 'returns the session id in the session hash' do
response = response_for(app: incrementor)
expect(response.body).to eq('{"counter"=>1}')
@ -423,38 +435,38 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to match(/"counter"=>1/)
end
it "does not return a cookie if set to secure but not using ssl" do
it 'does not return a cookie if set to secure but not using ssl' do
app = [incrementor, { secure: true }]
response = response_for(app: app)
expect(response["Set-Cookie"]).to be_nil
expect(response['Set-Cookie']).to be_nil
response = response_for(app: app, request: { "HTTPS" => "on" })
expect(response["Set-Cookie"]).to_not be_nil
expect(response["Set-Cookie"]).to match(/secure/)
response = response_for(app: app, request: { 'HTTPS' => 'on' })
expect(response['Set-Cookie']).to_not be_nil
expect(response['Set-Cookie']).to match(/secure/)
end
it "does not return a cookie if cookie was not read/written" do
it 'does not return a cookie if cookie was not read/written' do
response = response_for(app: nothing)
expect(response["Set-Cookie"]).to be_nil
expect(response['Set-Cookie']).to be_nil
end
it "does not return a cookie if cookie was not written (only read)" do
it 'does not return a cookie if cookie was not written (only read)' do
response = response_for(app: session_id)
expect(response["Set-Cookie"]).to be_nil
expect(response['Set-Cookie']).to be_nil
end
it "returns even if not read/written if :expire_after is set" do
it 'returns even if not read/written if :expire_after is set' do
app = [nothing, { expire_after: 3600 }]
request = { "rack.session" => { "not" => "empty" }}
request = { 'rack.session' => { 'not' => 'empty' } }
response = response_for(app: app, request: request)
expect(response["Set-Cookie"]).to_not be_nil
expect(response['Set-Cookie']).to_not be_nil
end
it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
it 'returns no cookie if no data was written and no session was created previously, even if :expire_after is set' do
app = [nothing, { expire_after: 3600 }]
response = response_for(app: app)
expect(response["Set-Cookie"]).to be_nil
expect(response['Set-Cookie']).to be_nil
end
it "exposes :secret in env['rack.session.option']" do
@ -472,14 +484,14 @@ RSpec.describe Rack::Protection::EncryptedCookie do
expect(response.body).to match(/Marshal/)
end
it "allows passing in a hash with session data from middleware in front" do
request = { 'rack.session' => { foo: 'bar' }}
it 'allows passing in a hash with session data from middleware in front' do
request = { 'rack.session' => { foo: 'bar' } }
response = response_for(app: session_id, request: request)
expect(response.body).to match(/foo/)
end
it "allows modifying session data with session data from middleware in front" do
request = { 'rack.session' => { foo: 'bar' }}
it 'allows modifying session data with session data from middleware in front' do
request = { 'rack.session' => { foo: 'bar' } }
response = response_for(app: incrementor, request: request)
expect(response.body).to match(/counter/)
expect(response.body).to match(/foo/)
@ -488,41 +500,42 @@ RSpec.describe Rack::Protection::EncryptedCookie do
it "allows more than one '--' in the cookie when calculating legacy digests" do
@counter = 0
app = lambda do |env|
env["rack.session"]["message"] ||= ""
env["rack.session"]["message"] << "#{(@counter += 1).to_s}--"
hash = env["rack.session"].dup
hash.delete("session_id")
Rack::Response.new(hash["message"]).to_a
env['rack.session']['message'] ||= ''
env['rack.session']['message'] << "#{@counter += 1}--"
hash = env['rack.session'].dup
hash.delete('session_id')
Rack::Response.new(hash['message']).to_a
end
# another example of an unsafe coder is Base64.urlsafe_encode64
unsafe_coder = Class.new {
unsafe_coder = Class.new do
def encode(hash); hash.inspect end
def decode(str); eval(str) if str; end
}.new
end.new
legacy_session = unsafe_coder.encode('message' => "#{@counter += 1}--#{@counter += 1}--", 'session_id' => 'abcdef')
legacy_secret = 'test legacy secret'
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, legacy_secret, legacy_session)
legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session)
legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly"
_app = [ app, {
_app = [app, {
secret: secret, legacy_hmac_secret: legacy_secret,
legacy_hmac_coder: unsafe_coder } ]
legacy_hmac_coder: unsafe_coder
}]
response = response_for(app: _app, cookie: legacy_cookie)
expect(response.body).to eq("1--2--3--")
expect(response.body).to eq('1--2--3--')
end
it 'allows for non-strict encoded cookie' do
long_session_app = lambda do |env|
env['rack.session']['value'] = 'A' * 256
env['rack.session']['counter'] = 1
hash = env["rack.session"].dup
hash.delete("session_id")
hash = env['rack.session'].dup
hash.delete('session_id')
Rack::Response.new(hash.inspect).to_a
end
non_strict_coder = Class.new {
non_strict_coder = Class.new do
def encode(str)
[Marshal.dump(str)].pack('m')
end
@ -530,19 +543,19 @@ RSpec.describe Rack::Protection::EncryptedCookie do
def decode(str)
return unless str
Marshal.load(str.unpack('m').first)
Marshal.load(str.unpack1('m'))
end
}.new
end.new
non_strict_response = response_for(app: [
long_session_app, { coder: non_strict_coder }
])
long_session_app, { coder: non_strict_coder }
])
response = response_for(app: [
incrementor
], cookie: non_strict_response)
incrementor
], cookie: non_strict_response)
expect(response.body).to match(%Q["value"=>"#{'A' * 256}"])
expect(response.body).to match(%("value"=>"#{'A' * 256}"))
expect(response.body).to match('"counter"=>2')
expect(response.body).to match(/\A{[^}]+}\z/)
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::Encryptor do
let(:secret) {
let(:secret) do
OpenSSL::Cipher.new(Rack::Protection::Encryptor::CIPHER).random_key
}
end
it 'encrypted message contains ciphertext iv and auth_tag' do
msg = Rack::Protection::Encryptor.encrypt_message('hello world', secret)

View File

@ -1,37 +1,39 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::EscapedParams do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
context 'escaping' do
it 'escapes html entities' do
mock_app do |env|
request = Rack::Request.new(env)
[200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]]
end
get '/', :foo => "<bar>"
get '/', foo: '<bar>'
expect(body).to eq('&lt;bar&gt;')
end
it 'leaves normal params untouched' do
mock_app do |env|
request = Rack::Request.new(env)
[200, {'Content-Type' => 'text/plain'}, [request.params['foo']]]
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]]
end
get '/', :foo => "bar"
get '/', foo: 'bar'
expect(body).to eq('bar')
end
it 'copes with nested arrays' do
mock_app do |env|
request = Rack::Request.new(env)
[200, {'Content-Type' => 'text/plain'}, [request.params['foo']['bar']]]
[200, { 'Content-Type' => 'text/plain' }, [request.params['foo']['bar']]]
end
get '/', :foo => {:bar => "<bar>"}
get '/', foo: { bar: '<bar>' }
expect(body).to eq('&lt;bar&gt;')
end
it 'leaves cache-breaker params untouched' do
mock_app do |env|
[200, {'Content-Type' => 'text/plain'}, ['hi']]
mock_app do |_env|
[200, { 'Content-Type' => 'text/plain' }, ['hi']]
end
get '/?95df8d9bf5237ad08df3115ee74dcb10'
@ -41,9 +43,7 @@ RSpec.describe Rack::Protection::EscapedParams do
it 'leaves TempFiles untouched' do
mock_app do |env|
request = Rack::Request.new(env)
[200, {'Content-Type' => 'text/plain'}, [request.params['file'][:filename] + "\n" + \
request.params['file'][:tempfile].read + "\n" + \
request.params['other']]]
[200, { 'Content-Type' => 'text/plain' }, ["#{request.params['file'][:filename]}\n#{request.params['file'][:tempfile].read}\n#{request.params['other']}"]]
end
temp_file = File.open('_escaped_params_tmp_file', 'w')
@ -51,7 +51,7 @@ RSpec.describe Rack::Protection::EscapedParams do
temp_file.write('hello world')
temp_file.close
post '/', :file => Rack::Test::UploadedFile.new(temp_file.path), :other => '<bar>'
post '/', file: Rack::Test::UploadedFile.new(temp_file.path), other: '<bar>'
expect(body).to eq("_escaped_params_tmp_file\nhello world\n&lt;bar&gt;")
ensure
File.unlink(temp_file.path)

View File

@ -1,46 +1,48 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::FormToken do
let(:token) { described_class.random_token }
let(:masked_token) { described_class.token(session) }
let(:bad_token) { Base64.strict_encode64("badtoken") }
let(:session) { {:csrf => token} }
let(:bad_token) { Base64.strict_encode64('badtoken') }
let(:session) { { csrf: token } }
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it "denies post requests without any token" do
it 'denies post requests without any token' do
expect(post('/')).not_to be_ok
end
it "accepts post requests with correct X-CSRF-Token header" do
it 'accepts post requests with correct X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end
it "accepts post requests with masked X-CSRF-Token header" do
it 'accepts post requests with masked X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token)
expect(last_response).to be_ok
end
it "denies post requests with wrong X-CSRF-Token header" do
it 'denies post requests with wrong X-CSRF-Token header' do
post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end
it "accepts post form requests with correct authenticity_token field" do
post('/', {"authenticity_token" => token}, 'rack.session' => session)
it 'accepts post form requests with correct authenticity_token field' do
post('/', { 'authenticity_token' => token }, 'rack.session' => session)
expect(last_response).to be_ok
end
it "accepts post form requests with masked authenticity_token field" do
post('/', {"authenticity_token" => masked_token}, 'rack.session' => session)
it 'accepts post form requests with masked authenticity_token field' do
post('/', { 'authenticity_token' => masked_token }, 'rack.session' => session)
expect(last_response).to be_ok
end
it "denies post form requests with wrong authenticity_token field" do
post('/', {"authenticity_token" => bad_token}, 'rack.session' => session)
it 'denies post form requests with wrong authenticity_token field' do
post('/', { 'authenticity_token' => bad_token }, 'rack.session' => session)
expect(last_response).not_to be_ok
end
it "accepts ajax requests without a valid token" do
expect(post('/', {}, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")).to be_ok
it 'accepts ajax requests without a valid token' do
expect(post('/', {}, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).to be_ok
end
end

View File

@ -1,37 +1,38 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::FrameOptions do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'should set the X-Frame-Options' do
expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("SAMEORIGIN")
expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('SAMEORIGIN')
end
it 'should not set the X-Frame-Options for other content types' do
expect(get('/', {}, 'wants' => 'text/foo').headers["X-Frame-Options"]).to be_nil
expect(get('/', {}, 'wants' => 'text/foo').headers['X-Frame-Options']).to be_nil
end
it 'should allow changing the protection mode' do
# I have no clue what other modes are available
mock_app do
use Rack::Protection::FrameOptions, :frame_options => :deny
use Rack::Protection::FrameOptions, frame_options: :deny
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("DENY")
expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('DENY')
end
it 'should allow changing the protection mode to a string' do
# I have no clue what other modes are available
mock_app do
use Rack::Protection::FrameOptions, :frame_options => "ALLOW-FROM foo"
use Rack::Protection::FrameOptions, frame_options: 'ALLOW-FROM foo'
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("ALLOW-FROM foo")
expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('ALLOW-FROM foo')
end
it 'should not override the header if already set' do
mock_app with_headers("X-Frame-Options" => "allow")
expect(get('/', {}, 'wants' => 'text/html').headers["X-Frame-Options"]).to eq("allow")
mock_app with_headers('X-Frame-Options' => 'allow')
expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('allow')
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::HttpOrigin do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
before(:each) do
mock_app do
@ -8,29 +10,29 @@ RSpec.describe Rack::Protection::HttpOrigin do
end
end
%w(GET HEAD POST PUT DELETE).each do |method|
%w[GET HEAD POST PUT DELETE].each do |method|
it "accepts #{method} requests with no Origin" do
expect(send(method.downcase, '/')).to be_ok
end
end
%w(GET HEAD).each do |method|
%w[GET HEAD].each do |method|
it "accepts #{method} requests with non-permitted Origin" do
expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).to be_ok
end
end
%w(GET HEAD POST PUT DELETE).each do |method|
%w[GET HEAD POST PUT DELETE].each do |method|
it "accepts #{method} requests when allow_if is true" do
mock_app do
use Rack::Protection::HttpOrigin, :allow_if => lambda{|env| env.has_key?('HTTP_ORIGIN') }
use Rack::Protection::HttpOrigin, allow_if: ->(env) { env.key?('HTTP_ORIGIN') }
run DummyApp
end
expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://any.domain.com')).to be_ok
end
end
%w(POST PUT DELETE).each do |method|
%w[POST PUT DELETE].each do |method|
it "denies #{method} requests with non-permitted Origin" do
expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).not_to be_ok
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::IPSpoofing do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'accepts requests without X-Forward-For header' do
get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_REAL_IP' => '4.3.2.1')
@ -8,7 +10,7 @@ RSpec.describe Rack::Protection::IPSpoofing do
it 'accepts requests with proper X-Forward-For header' do
get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4',
'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
expect(last_response).to be_ok
end
@ -19,15 +21,15 @@ RSpec.describe Rack::Protection::IPSpoofing do
it 'denies requests where the client spoofs the IP but not X-Forward-For' do
get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.5',
'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1')
expect(last_response).not_to be_ok
end
it 'denies requests where IP and X-Forward-For are spoofed but not X-Real-IP' do
get('/', {},
'HTTP_CLIENT_IP' => '1.2.3.5',
'HTTP_X_FORWARDED_FOR' => '1.2.3.5',
'HTTP_X_REAL_IP' => '1.2.3.4')
'HTTP_CLIENT_IP' => '1.2.3.5',
'HTTP_X_FORWARDED_FOR' => '1.2.3.5',
'HTTP_X_REAL_IP' => '1.2.3.4')
expect(last_response).not_to be_ok
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::JsonCsrf do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
module DummyAppWithBody
module Closeable
@ -22,22 +24,22 @@ RSpec.describe Rack::Protection::JsonCsrf do
def self.call(env)
Thread.current[:last_env] = env
[200, {'Content-Type' => 'application/json'}, body]
[200, { 'Content-Type' => 'application/json' }, body]
end
end
describe 'json response' do
before do
mock_app { |e| [200, {'Content-Type' => 'application/json'}, []]}
mock_app { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
end
it "denies get requests with json responses with a remote referrer" do
it 'denies get requests with json responses with a remote referrer' do
expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com')).not_to be_ok
end
it "closes the body returned by the app if it denies the get request" do
mock_app DummyAppWithBody do |e|
[200, {'Content-Type' => 'application/json'}, []]
it 'closes the body returned by the app if it denies the get request' do
mock_app DummyAppWithBody do |_e|
[200, { 'Content-Type' => 'application/json' }, []]
end
get('/', {}, 'HTTP_REFERER' => 'http://evil.com')
@ -45,10 +47,10 @@ RSpec.describe Rack::Protection::JsonCsrf do
expect(DummyAppWithBody.body).to be_closed
end
it "accepts requests with json responses with a remote referrer when allow_if is true" do
it 'accepts requests with json responses with a remote referrer when allow_if is true' do
mock_app do
use Rack::Protection::JsonCsrf, :allow_if => lambda{|env| env['HTTP_REFERER'] == 'http://good.com'}
run proc { |e| [200, {'Content-Type' => 'application/json'}, []]}
use Rack::Protection::JsonCsrf, allow_if: ->(env) { env['HTTP_REFERER'] == 'http://good.com' }
run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
end
expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com')).to be_ok
@ -62,37 +64,34 @@ RSpec.describe Rack::Protection::JsonCsrf do
expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_X_ORIGIN' => 'http://good.com')).to be_ok
end
it "accepts get requests with json responses with a local referrer" do
it 'accepts get requests with json responses with a local referrer' do
expect(get('/', {}, 'HTTP_REFERER' => '/')).to be_ok
end
it "accepts get requests with json responses with no referrer" do
it 'accepts get requests with json responses with no referrer' do
expect(get('/', {})).to be_ok
end
it "accepts XHR requests" do
it 'accepts XHR requests' do
expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).to be_ok
end
end
describe 'not json response' do
it "accepts get requests with 304 headers" do
mock_app { |e| [304, {}, []]}
it 'accepts get requests with 304 headers' do
mock_app { |_e| [304, {}, []] }
expect(get('/', {}).status).to eq(304)
end
end
describe 'with drop_session as default reaction' do
it 'still denies' do
mock_app do
use Rack::Protection, :reaction => :drop_session
run proc { |e| [200, {'Content-Type' => 'application/json'}, []]}
use Rack::Protection, reaction: :drop_session
run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] }
end
session = {:foo => :bar}
session = { foo: :bar }
get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'rack.session' => session)
expect(last_response).not_to be_ok
end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::PathTraversal do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
context 'escaping' do
before do
mock_app { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO']]] }
mock_app { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO']]] }
end
%w[/foo/bar /foo/bar/ / /.f /a.x].each do |path|
@ -26,7 +28,7 @@ RSpec.describe Rack::Protection::PathTraversal do
context "PATH_INFO's encoding" do
before do
@app = Rack::Protection::PathTraversal.new(proc { |e| [200, {'Content-Type' => 'text/plain'}, [e['PATH_INFO'].encoding.to_s]] })
@app = Rack::Protection::PathTraversal.new(proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO'].encoding.to_s]] })
end
it 'should remain unchanged as ASCII-8BIT' do

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'passes on options' do
mock_app do
use Rack::Protection, :track => ['HTTP_FOO']
run proc { |e| [200, {'Content-Type' => 'text/plain'}, ['hi']] }
use Rack::Protection, track: ['HTTP_FOO']
run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] }
end
session = {:foo => :bar}
session = { foo: :bar }
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
expect(session[:foo]).to eq(:bar)
@ -18,21 +20,21 @@ RSpec.describe Rack::Protection do
it 'passes errors through if :reaction => :report is used' do
mock_app do
use Rack::Protection, :reaction => :report
run proc { |e| [200, {'Content-Type' => 'text/plain'}, [e["protection.failed"].to_s]] }
use Rack::Protection, reaction: :report
run proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['protection.failed'].to_s]] }
end
session = {:foo => :bar}
session = { foo: :bar }
post('/', {}, 'rack.session' => session, 'HTTP_ORIGIN' => 'http://malicious.com')
expect(last_response).to be_ok
expect(body).to eq("true")
expect(body).to eq('true')
end
describe "#react" do
describe '#react' do
it 'prevents attacks and warns about it' do
io = StringIO.new
mock_app do
use Rack::Protection, :logger => Logger.new(io)
use Rack::Protection, logger: Logger.new(io)
run DummyApp
end
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
@ -42,7 +44,7 @@ RSpec.describe Rack::Protection do
it 'reports attacks if reaction is to report' do
io = StringIO.new
mock_app do
use Rack::Protection, :reaction => :report, :logger => Logger.new(io)
use Rack::Protection, reaction: :report, logger: Logger.new(io)
run DummyApp
end
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
@ -54,7 +56,7 @@ RSpec.describe Rack::Protection do
io = StringIO.new
Rack::Protection::Base.send(:define_method, :special) { |*args| io << args.inspect }
mock_app do
use Rack::Protection, :reaction => :special, :logger => Logger.new(io)
use Rack::Protection, reaction: :special, logger: Logger.new(io)
run DummyApp
end
post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com')
@ -63,34 +65,34 @@ RSpec.describe Rack::Protection do
end
end
describe "#html?" do
context "given an appropriate content-type header" do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => "text/html" }
describe '#html?' do
context 'given an appropriate content-type header' do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'text/html' }
it { is_expected.to be_truthy }
end
context "given an appropriate content-type header of text/xml" do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => "text/xml" }
context 'given an appropriate content-type header of text/xml' do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'text/xml' }
it { is_expected.to be_truthy }
end
context "given an appropriate content-type header of application/xml" do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => "application/xml" }
context 'given an appropriate content-type header of application/xml' do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'application/xml' }
it { is_expected.to be_truthy }
end
context "given an inappropriate content-type header" do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => "image/gif" }
context 'given an inappropriate content-type header' do
subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'image/gif' }
it { is_expected.to be_falsey }
end
context "given no content-type header" do
context 'given no content-type header' do
subject { Rack::Protection::Base.new(nil).html?({}) }
it { is_expected.to be_falsey }
end
end
describe "#instrument" do
describe '#instrument' do
let(:env) { { 'rack.protection.attack' => 'base' } }
let(:instrumenter) { double('Instrumenter') }
@ -99,7 +101,7 @@ RSpec.describe Rack::Protection do
end
context 'with an instrumenter specified' do
let(:app) { Rack::Protection::Base.new(nil, :instrumenter => instrumenter) }
let(:app) { Rack::Protection::Base.new(nil, instrumenter: instrumenter) }
it { expect(instrumenter).to receive(:instrument).with('rack.protection', env) }
end
@ -111,14 +113,14 @@ RSpec.describe Rack::Protection do
end
end
describe "new" do
describe 'new' do
it 'should allow disable session protection' do
mock_app do
use Rack::Protection, :without_session => true
use Rack::Protection, without_session: true
run DummyApp
end
session = {:foo => :bar}
session = { foo: :bar }
get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'a'
get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'b'
expect(session[:foo]).to eq :bar
@ -126,7 +128,7 @@ RSpec.describe Rack::Protection do
it 'should allow disable CSRF protection' do
mock_app do
use Rack::Protection, :without_session => true
use Rack::Protection, without_session: true
run DummyApp
end

View File

@ -1,28 +1,30 @@
RSpec.describe Rack::Protection::RemoteReferrer do
it_behaves_like "any rack application"
# frozen_string_literal: true
it "accepts post requests with no referrer" do
RSpec.describe Rack::Protection::RemoteReferrer do
it_behaves_like 'any rack application'
it 'accepts post requests with no referrer' do
expect(post('/')).to be_ok
end
it "does not accept post requests with no referrer if allow_empty_referrer is false" do
it 'does not accept post requests with no referrer if allow_empty_referrer is false' do
mock_app do
use Rack::Protection::RemoteReferrer, :allow_empty_referrer => false
use Rack::Protection::RemoteReferrer, allow_empty_referrer: false
run DummyApp
end
expect(post('/')).not_to be_ok
end
it "should allow post request with a relative referrer" do
it 'should allow post request with a relative referrer' do
expect(post('/', {}, 'HTTP_REFERER' => '/')).to be_ok
end
it "accepts post requests with the same host in the referrer" do
it 'accepts post requests with the same host in the referrer' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.com')
expect(last_response).to be_ok
end
it "denies post requests with a remote referrer" do
it 'denies post requests with a remote referrer' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org')
expect(last_response).not_to be_ok
end

View File

@ -1,57 +1,59 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::RemoteToken do
let(:token) { described_class.random_token }
let(:masked_token) { described_class.token(session) }
let(:bad_token) { Base64.strict_encode64("badtoken") }
let(:session) { {:csrf => token} }
let(:bad_token) { Base64.strict_encode64('badtoken') }
let(:session) { { csrf: token } }
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it "accepts post requests with no referrer" do
it 'accepts post requests with no referrer' do
expect(post('/')).to be_ok
end
it "accepts post requests with a local referrer" do
it 'accepts post requests with a local referrer' do
expect(post('/', {}, 'HTTP_REFERER' => '/')).to be_ok
end
it "denies post requests with a remote referrer and no token" do
it 'denies post requests with a remote referrer and no token' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org')
expect(last_response).not_to be_ok
end
it "accepts post requests with a remote referrer and correct X-CSRF-Token header" do
it 'accepts post requests with a remote referrer and correct X-CSRF-Token header' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token)
expect(last_response).to be_ok
end
it "accepts post requests with a remote referrer and masked X-CSRF-Token header" do
it 'accepts post requests with a remote referrer and masked X-CSRF-Token header' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token)
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token)
expect(last_response).to be_ok
end
it "denies post requests with a remote referrer and wrong X-CSRF-Token header" do
it 'denies post requests with a remote referrer and wrong X-CSRF-Token header' do
post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org',
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token)
expect(last_response).not_to be_ok
end
it "accepts post form requests with a remote referrer and correct authenticity_token field" do
post('/', {"authenticity_token" => token}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
it 'accepts post form requests with a remote referrer and correct authenticity_token field' do
post('/', { 'authenticity_token' => token }, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).to be_ok
end
it "accepts post form requests with a remote referrer and masked authenticity_token field" do
post('/', {"authenticity_token" => masked_token}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
it 'accepts post form requests with a remote referrer and masked authenticity_token field' do
post('/', { 'authenticity_token' => masked_token }, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).to be_ok
end
it "denies post form requests with a remote referrer and wrong authenticity_token field" do
post('/', {"authenticity_token" => bad_token}, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
it 'denies post form requests with a remote referrer and wrong authenticity_token field' do
post('/', { 'authenticity_token' => bad_token }, 'HTTP_REFERER' => 'http://example.com/foo',
'HTTP_HOST' => 'example.org', 'rack.session' => session)
expect(last_response).not_to be_ok
end
end

View File

@ -1,30 +1,32 @@
RSpec.describe Rack::Protection::SessionHijacking do
it_behaves_like "any rack application"
# frozen_string_literal: true
it "accepts a session without changes to tracked parameters" do
session = {:foo => :bar}
RSpec.describe Rack::Protection::SessionHijacking do
it_behaves_like 'any rack application'
it 'accepts a session without changes to tracked parameters' do
session = { foo: :bar }
get '/', {}, 'rack.session' => session
get '/', {}, 'rack.session' => session
expect(session[:foo]).to eq(:bar)
end
it "denies requests with a changing User-Agent header" do
session = {:foo => :bar}
it 'denies requests with a changing User-Agent header' do
session = { foo: :bar }
get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'a'
get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'b'
expect(session).to be_empty
end
it "accepts requests with a changing Accept-Encoding header" do
it 'accepts requests with a changing Accept-Encoding header' do
# this is tested because previously it led to clearing the session
session = {:foo => :bar}
session = { foo: :bar }
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a'
get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b'
expect(session).not_to be_empty
end
it "accepts requests with a changing Version header"do
session = {:foo => :bar}
it 'accepts requests with a changing Version header' do
session = { foo: :bar }
get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.0'
get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.1'
expect(session[:foo]).to eq(:bar)

View File

@ -1,43 +1,45 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::StrictTransport do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'should set the Strict-Transport-Security header' do
expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000")
expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000')
end
it 'should allow changing the max-age option' do
mock_app do
use Rack::Protection::StrictTransport, :max_age => 16_070_400
use Rack::Protection::StrictTransport, max_age: 16_070_400
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=16070400")
expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=16070400')
end
it 'should allow switching on the include_subdomains option' do
mock_app do
use Rack::Protection::StrictTransport, :include_subdomains => true
use Rack::Protection::StrictTransport, include_subdomains: true
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000; includeSubDomains")
expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; includeSubDomains')
end
it 'should allow switching on the preload option' do
mock_app do
use Rack::Protection::StrictTransport, :preload => true
use Rack::Protection::StrictTransport, preload: true
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000; preload")
expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; preload')
end
it 'should allow switching on all the options' do
mock_app do
use Rack::Protection::StrictTransport, :preload => true, :include_subdomains => true
use Rack::Protection::StrictTransport, preload: true, include_subdomains: true
run DummyApp
end
expect(get('/', {}, 'wants' => 'text/html').headers["Strict-Transport-Security"]).to eq("max-age=31536000; includeSubDomains; preload")
expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; includeSubDomains; preload')
end
end

View File

@ -1,54 +1,54 @@
# frozen_string_literal: true
RSpec.describe Rack::Protection::XSSHeader do
it_behaves_like "any rack application"
it_behaves_like 'any rack application'
it 'should set the X-XSS-Protection' do
expect(get('/', {}, 'wants' => 'text/html;charset=utf-8').headers["X-XSS-Protection"]).to eq("1; mode=block")
expect(get('/', {}, 'wants' => 'text/html;charset=utf-8').headers['X-XSS-Protection']).to eq('1; mode=block')
end
it 'should set the X-XSS-Protection for XHTML' do
expect(get('/', {}, 'wants' => 'application/xhtml+xml').headers["X-XSS-Protection"]).to eq("1; mode=block")
expect(get('/', {}, 'wants' => 'application/xhtml+xml').headers['X-XSS-Protection']).to eq('1; mode=block')
end
it 'should not set the X-XSS-Protection for other content types' do
expect(get('/', {}, 'wants' => 'application/foo').headers["X-XSS-Protection"]).to be_nil
expect(get('/', {}, 'wants' => 'application/foo').headers['X-XSS-Protection']).to be_nil
end
it 'should allow changing the protection mode' do
# I have no clue what other modes are available
mock_app do
use Rack::Protection::XSSHeader, :xss_mode => :foo
use Rack::Protection::XSSHeader, xss_mode: :foo
run DummyApp
end
expect(get('/', {}, 'wants' => 'application/xhtml').headers["X-XSS-Protection"]).to eq("1; mode=foo")
expect(get('/', {}, 'wants' => 'application/xhtml').headers['X-XSS-Protection']).to eq('1; mode=foo')
end
it 'should not override the header if already set' do
mock_app with_headers("X-XSS-Protection" => "0")
expect(get('/', {}, 'wants' => 'text/html').headers["X-XSS-Protection"]).to eq("0")
mock_app with_headers('X-XSS-Protection' => '0')
expect(get('/', {}, 'wants' => 'text/html').headers['X-XSS-Protection']).to eq('0')
end
it 'should set the X-Content-Type-Options' do
expect(get('/', {}, 'wants' => 'text/html').header["X-Content-Type-Options"]).to eq("nosniff")
expect(get('/', {}, 'wants' => 'text/html').header['X-Content-Type-Options']).to eq('nosniff')
end
it 'should set the X-Content-Type-Options for other content types' do
expect(get('/', {}, 'wants' => 'application/foo').header["X-Content-Type-Options"]).to eq("nosniff")
expect(get('/', {}, 'wants' => 'application/foo').header['X-Content-Type-Options']).to eq('nosniff')
end
it 'should allow changing the nosniff-mode off' do
mock_app do
use Rack::Protection::XSSHeader, :nosniff => false
use Rack::Protection::XSSHeader, nosniff: false
run DummyApp
end
expect(get('/').headers["X-Content-Type-Options"]).to be_nil
expect(get('/').headers['X-Content-Type-Options']).to be_nil
end
it 'should not override the header if already set X-Content-Type-Options' do
mock_app with_headers("X-Content-Type-Options" => "sniff")
expect(get('/', {}, 'wants' => 'text/html').headers["X-Content-Type-Options"]).to eq("sniff")
mock_app with_headers('X-Content-Type-Options' => 'sniff')
expect(get('/', {}, 'wants' => 'text/html').headers['X-Content-Type-Options']).to eq('sniff')
end
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
require 'rack/protection'
require 'rack/test'
require 'rack'
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f }
# This file was generated by the `rspec --init` command. Conventionally, all
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
module DummyApp
def self.call(env)
Thread.current[:last_env] = env
body = (env['REQUEST_METHOD'] == 'HEAD' ? '' : 'ok')
[200, {'Content-Type' => env['wants'] || 'text/plain'}, [body]]
[200, { 'Content-Type' => env['wants'] || 'text/plain' }, [body]]
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# see http://blog.101ideas.cz/posts/pending-examples-via-not-implemented-error-in-rspec.html
module NotImplementedAsPending
def self.included(base)
@ -20,4 +22,4 @@ module NotImplementedAsPending
end
RSpec::Core::Example.send :include, self
end
end

View File

@ -1,12 +1,15 @@
if defined? Gem.loaded_specs and Gem.loaded_specs.include? 'rack'
version = Gem.loaded_specs['rack'].version.to_s
else
version = Rack.release + '.0'
end
# frozen_string_literal: true
if version == "1.3"
version = if defined? Gem.loaded_specs&.include?('rack')
Gem.loaded_specs['rack'].version.to_s
else
"#{Rack.release}.0"
end
if version == '1.3'
Rack::Session::Abstract::ID.class_eval do
private
def prepare_session(env)
session_was = env[ENV_SESSION_KEY]
env[ENV_SESSION_KEY] = SessionHash.new(self, env)

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true
RSpec.shared_examples_for 'any rack application' do
it "should not interfere with normal get requests" do
it 'should not interfere with normal get requests' do
expect(get('/')).to be_ok
expect(body).to eq('ok')
end
it "should not interfere with normal head requests" do
it 'should not interfere with normal head requests' do
expect(head('/')).to be_ok
end
@ -14,9 +16,10 @@ RSpec.shared_examples_for 'any rack application' do
def call(env)
was = env.dup
res = app.call(env)
was.each do |k,v|
was.each do |k, v|
next if env[k] == v
fail "env[#{k.inspect}] changed from #{v.inspect} to #{env[k].inspect}"
raise "env[#{k.inspect}] changed from #{v.inspect} to #{env[k].inspect}"
end
res
end
@ -24,13 +27,13 @@ RSpec.shared_examples_for 'any rack application' do
mock_app do
use Rack::Head
use(Rack::Config) { |e| e['rack.session'] ||= {}}
use(Rack::Config) { |e| e['rack.session'] ||= {} }
use detector
use klass
run DummyApp
end
expect(get('/..', :foo => '<bar>')).to be_ok
expect(get('/..', foo: '<bar>')).to be_ok
end
it 'allows passing on values in env' do
@ -53,7 +56,7 @@ RSpec.shared_examples_for 'any rack application' do
mock_app do
use Rack::Head
use(Rack::Config) { |e| e['rack.session'] ||= {}}
use(Rack::Config) { |e| e['rack.session'] ||= {} }
use changer
use klass
use detector

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'forwardable'
module SpecHelpers
@ -12,12 +14,12 @@ module SpecHelpers
end
def mock_app(app = nil, &block)
app = block if app.nil? and block.arity == 1
app = block if app.nil? && (block.arity == 1)
if app
klass = described_class
mock_app do
use Rack::Head
use(Rack::Config) { |e| e['rack.session'] ||= {}}
use(Rack::Config) { |e| e['rack.session'] ||= {} }
use klass
run app
end
@ -27,10 +29,10 @@ module SpecHelpers
end
def with_headers(headers)
proc { [200, {'Content-Type' => 'text/plain'}.merge(headers), ['ok']] }
proc { [200, { 'Content-Type' => 'text/plain' }.merge(headers), ['ok']] }
end
def env
Thread.current[:last_env]
end
end
end

View File

@ -1,8 +1,10 @@
source "https://rubygems.org"
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec
gem 'sinatra', path: '..'
gem 'rack-protection', path: '../rack-protection'
gem 'sinatra', path: '..'
gem 'rack-test', github: 'rack/rack-test'
@ -36,6 +38,6 @@ repos = { 'tilt' => 'rtomayko/tilt', 'rack' => 'rack/rack' }
%w[tilt rack].each do |lib|
dep = (ENV[lib] || 'stable').sub "#{lib}-", ''
dep = nil if dep == 'stable'
dep = {:github => repos[lib], :branch => dep} if dep and dep !~ /(\d+\.)+\d+/
dep = { github: repos[lib], branch: dep } if dep && dep !~ (/(\d+\.)+\d+/)
gem lib, dep if dep
end

View File

@ -1,12 +1,14 @@
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path('lib', __dir__)
require 'open-uri'
require 'yaml'
require 'sinatra/contrib/version'
desc "run specs"
desc 'run specs'
task(:spec) { ruby '-S rspec' }
task(:test => :spec)
task(:default => :spec)
task(test: :spec)
task(default: :spec)
namespace :doc do
task :readmes do
@ -14,36 +16,37 @@ namespace :doc do
puts "Trying file... #{file}"
excluded_files = %w[lib/sinatra/contrib.rb lib/sinatra/decompile.rb]
next if excluded_files.include?(file)
doc = File.read(file)[/^module Sinatra(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
Dir.mkdir "doc" unless File.directory? "doc"
file = "doc/#{file[4..-4].tr('/_', '-')}.rdoc"
Dir.mkdir 'doc' unless File.directory? 'doc'
puts "writing #{file}"
File.open(file, "w") { |f| f << doc }
File.open(file, 'w') { |f| f << doc }
end
end
task :index do
doc = File.read("README.md")
file = "doc/sinatra-contrib-readme.md"
Dir.mkdir "doc" unless File.directory? "doc"
doc = File.read('README.md')
file = 'doc/sinatra-contrib-readme.md'
Dir.mkdir 'doc' unless File.directory? 'doc'
puts "writing #{file}"
File.open(file, "w") { |f| f << doc }
File.open(file, 'w') { |f| f << doc }
end
task :all => [:readmes, :index]
task all: %i[readmes index]
end
desc "generate documentation"
task :doc => 'doc:all'
desc 'generate documentation'
task doc: 'doc:all'
desc "generate gemspec"
desc 'generate gemspec'
task 'sinatra-contrib.gemspec' do
content = File.read 'sinatra-contrib.gemspec'
fields = {
:authors => `git shortlog -sn`.scan(/[^\d\s].*/),
:email => `git shortlog -sne`.scan(/[^<]+@[^>]+/),
:files => `git ls-files`.split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
authors: `git shortlog -sn`.scan(/[^\d\s].*/),
email: `git shortlog -sne`.scan(/[^<]+@[^>]+/),
files: `git ls-files`.split("\n").grep_v(/^(\.|Gemfile)/)
}
fields.each do |field, values|
@ -56,9 +59,9 @@ task 'sinatra-contrib.gemspec' do
File.open('sinatra-contrib.gemspec', 'w') { |f| f << content }
end
task :gemspec => 'sinatra-contrib.gemspec'
task gemspec: 'sinatra-contrib.gemspec'
task :release => :gemspec do
task release: :gemspec do
sh <<-SH
rm -Rf sinatra-contrib*.gem &&
gem build sinatra-contrib.gemspec &&
@ -70,4 +73,3 @@ task :release => :gemspec do
git push --tags && (git push origin --tags || true)
SH
end

View File

@ -86,17 +86,19 @@ module Sinatra
def capture(*args, &block)
return block[*args] if ruby?
if haml? && Tilt[:haml] == Tilt::HamlTemplate
buffer = Haml::Buffer.new(nil, Haml::Options.new.for_buffer)
with_haml_buffer(buffer) { capture_haml(*args, &block) }
else
@_out_buf, _buf_was = '', @_out_buf
buf_was = @_out_buf
@_out_buf = ''
begin
raw = block[*args]
captured = block.binding.eval('@_out_buf')
captured.empty? ? raw : captured
ensure
@_out_buf = _buf_was
@_out_buf = buf_was
end
end
end

View File

@ -3,7 +3,6 @@ require 'yaml'
require 'erb'
module Sinatra
# = Sinatra::ConfigFile
#
# <tt>Sinatra::ConfigFile</tt> is an extension that allows you to load the
@ -107,7 +106,6 @@ module Sinatra
# bar: 'baz' # override the default value
#
module ConfigFile
# When the extension is registered sets the +environments+ setting to the
# traditional environments: development, test and production.
def self.registered(base)
@ -122,8 +120,9 @@ module Sinatra
paths.each do |pattern|
Dir.glob(pattern) do |file|
raise UnsupportedConfigType unless ['.yml', '.yaml', '.erb'].include?(File.extname(file))
logger.info "loading config file '#{file}'" if logging? && respond_to?(:logger)
document = ERB.new(IO.read(file)).result
document = ERB.new(File.read(file)).result
yaml = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(document) : YAML.load(document)
config = config_for_env(yaml)
config.each_pair { |key, value| set(key, value) }
@ -132,7 +131,7 @@ module Sinatra
end
end
class UnsupportedConfigType < Exception
class UnsupportedConfigType < StandardError
def message
'Invalid config file type, use .yml, .yaml or .erb'
end

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'sinatra/capture'
module Sinatra
# = Sinatra::ContentFor
#
# <tt>Sinatra::ContentFor</tt> is a set of helpers that allows you to capture
@ -174,7 +175,7 @@ module Sinatra
# for <tt>:head</tt>.
def yield_content(key, *args, &block)
if block_given? && !content_for?(key)
(haml? && Tilt[:haml] == Tilt::HamlTemplate) ? capture_haml(*args, &block) : yield(*args)
haml? && Tilt[:haml] == Tilt::HamlTemplate ? capture_haml(*args, &block) : yield(*args)
else
content = content_blocks[key.to_sym].map { |b| capture(*args, &b) }
content.join.tap do |c|

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/contrib/setup'
module Sinatra

View File

@ -1,2 +1,4 @@
# frozen_string_literal: true
require 'sinatra/contrib'
Sinatra.register Sinatra::Contrib::All

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'sinatra/contrib/version'
@ -5,7 +7,7 @@ module Sinatra
module Contrib
module Loader
def extensions
@extensions ||= {:helpers => [], :register => []}
@extensions ||= { helpers: [], register: [] }
end
def register(name, path)

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
module Sinatra
module Contrib
VERSION = '3.0.0'
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
@ -65,15 +67,15 @@ module Sinatra
@deleted = []
@options = {
:path => @request.script_name.to_s.empty? ? '/' : @request.script_name,
:domain => @request.host == 'localhost' ? nil : @request.host,
:secure => @request.secure?,
:httponly => true
path: @request.script_name.to_s.empty? ? '/' : @request.script_name,
domain: @request.host == 'localhost' ? nil : @request.host,
secure: @request.secure?,
httponly: true
}
if app.settings.respond_to? :cookie_options
@options.merge! app.settings.cookie_options
end
return unless app.settings.respond_to? :cookie_options
@options.merge! app.settings.cookie_options
end
def ==(other)
@ -88,9 +90,11 @@ module Sinatra
set(key, value: value)
end
def assoc(key)
to_hash.assoc(key.to_s)
end if Hash.method_defined? :assoc
if Hash.method_defined? :assoc
def assoc(key)
to_hash.assoc(key.to_s)
end
end
def clear
each_key { |k| delete(k) }
@ -114,17 +118,20 @@ module Sinatra
def delete_if
return enum_for(__method__) unless block_given?
each { |k, v| delete(k) if yield(k, v) }
self
end
def each(&block)
return enum_for(__method__) unless block_given?
to_hash.each(&block)
end
def each_key(&block)
return enum_for(__method__) unless block_given?
to_hash.each_key(&block)
end
@ -132,6 +139,7 @@ module Sinatra
def each_value(&block)
return enum_for(__method__) unless block_given?
to_hash.each_value(&block)
end
@ -145,16 +153,18 @@ module Sinatra
end
end
def flatten
to_hash.flatten
end if Hash.method_defined? :flatten
if Hash.method_defined? :flatten
def flatten
to_hash.flatten
end
end
def has_key?(key)
response_cookies.has_key? key.to_s or request_cookies.has_key? key.to_s
response_cookies.key? key.to_s or request_cookies.key? key.to_s
end
def has_value?(value)
response_cookies.has_value? value or request_cookies.has_value? value
response_cookies.value? value or request_cookies.value? value
end
def hash
@ -168,13 +178,16 @@ module Sinatra
"<##{self.class}: #{to_hash.inspect[1..-2]}>"
end
def invert
to_hash.invert
end if Hash.method_defined? :invert
if Hash.method_defined? :invert
def invert
to_hash.invert
end
end
def keep_if
return enum_for(__method__) unless block_given?
delete_if { |*a| not yield(*a) }
delete_if { |*a| !yield(*a) }
end
def key(value)
@ -197,11 +210,11 @@ module Sinatra
def merge!(other)
other.each_pair do |key, value|
if block_given? and include? key
self[key] = yield(key.to_s, self[key], value)
else
self[key] = value
end
self[key] = if block_given? && include?(key)
yield(key.to_s, self[key], value)
else
value
end
end
end
@ -217,18 +230,20 @@ module Sinatra
def reject(&block)
return enum_for(__method__) unless block_given?
to_hash.reject(&block)
end
alias reject! delete_if
def replace(other)
select! { |k, v| other.include?(k) or other.include?(k.to_s) }
select! { |k, _v| other.include?(k) or other.include?(k.to_s) }
merge! other
end
def select(&block)
return enum_for(__method__) unless block_given?
to_hash.select(&block)
end
@ -246,9 +261,11 @@ module Sinatra
alias size length
def sort(&block)
to_hash.sort(&block)
end if Hash.method_defined? :sort
if Hash.method_defined? :sort
def sort(&block)
to_hash.sort(&block)
end
end
alias store []=
@ -300,6 +317,7 @@ module Sinatra
string.each_line do |line|
key, value = line.split(';', 2).first.to_s.split('=', 2)
next if key.nil?
key = Rack::Utils.unescape(key)
if line =~ /expires=Thu, 01[-\s]Jan[-\s]1970/
@deleted << key
@ -314,7 +332,7 @@ module Sinatra
end
def request_cookies
@request.cookies.reject { |key, value| deleted.include? key }
@request.cookies.reject { |key, _value| deleted.include? key }
end
end

View File

@ -1,5 +1,6 @@
module Sinatra
# frozen_string_literal: true
module Sinatra
# = Sinatra::CustomLogger
#
# CustomLogger extension allows you to define your own logger instance

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
@ -17,7 +19,7 @@ module Sinatra
# @return [Boolean] Returns true if current engine is `:erubi`.
def erubi?
@current_engine == :erubi or
erb? && Tilt[:erb] == Tilt::ErubiTemplate
(erb? && Tilt[:erb] == Tilt::ErubiTemplate)
end
# @return [Boolean] Returns true if current engine is `:haml`.
@ -72,7 +74,8 @@ module Sinatra
# @param engine [Symbol, String] Name of Engine to shift to.
def with_engine(engine)
@current_engine, engine_was = engine.to_sym, @current_engine
engine_was = @current_engine
@current_engine = engine.to_sym
yield
ensure
@current_engine = engine_was

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::Extension
#
# <tt>Sinatra::Extension</tt> is a mixin that provides some syntactic sugar
@ -81,13 +82,14 @@ module Sinatra
def method_missing(method, *args, &block)
return super unless Sinatra::Base.respond_to? method
record(method, *args, &block)
DontCall.new(method)
end
class DontCall < BasicObject
def initialize(method) @method = method end
def method_missing(*) fail "not supposed to use result of #@method!" end
def method_missing(*) raise "not supposed to use result of #{@method}!" end
def inspect; "#<#{self.class}: #{@method}>" end
end
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'multi_json'
module Sinatra
# = Sinatra::JSON
#
# <tt>Sinatra::JSON</tt> adds a helper method, called +json+, for (obviously)
@ -95,7 +96,7 @@ module Sinatra
def json(object, options = {})
content_type resolve_content_type(options)
resolve_encoder_action object, resolve_encoder(options)
resolve_encoder_action object, resolve_encoder(options)
end
private
@ -109,16 +110,14 @@ module Sinatra
end
def resolve_encoder_action(object, encoder)
[:encode, :generate].each do |method|
%i[encode generate].each do |method|
return encoder.send(method, object) if encoder.respond_to? method
end
if encoder.is_a? Symbol
object.__send__(encoder)
else
fail "#{encoder} does not respond to #generate nor #encode"
end #if
end #resolve_encoder_action
end #JSON
raise "#{encoder} does not respond to #generate nor #encode" unless encoder.is_a? Symbol
object.__send__(encoder)
end
end
Base.set :json_encoder do
::MultiJson

View File

@ -1,7 +1,6 @@
require 'sinatra/base'
module Sinatra
# = Sinatra::LinkHeader
#
# <tt>Sinatra::LinkHeader</tt> adds a set of helper methods to generate link
@ -86,8 +85,8 @@ module Sinatra
opts[:rel] = urls.shift unless urls.first.respond_to? :to_str
options = opts.map { |k, v| " #{k}=#{v.to_s.inspect}" }
html_pattern = "<link href=\"%s\"#{options.join} />"
http_pattern = ["<%s>", *options].join ";"
link = (response["Link"] ||= "")
http_pattern = ['<%s>', *options].join ';'
link = (response['Link'] ||= '')
urls.map do |url|
link << ",\n" unless link.empty?
@ -116,14 +115,15 @@ module Sinatra
# %body= yield
def link_headers
yield if block_given?
return "" unless response.include? "Link"
response["Link"].split(",\n").map do |line|
return '' unless response.include? 'Link'
response['Link'].split(",\n").map do |line|
url, *opts = line.split(';').map(&:strip)
"<link href=\"#{url[1..-2]}\" #{opts.join " "} />"
"<link href=\"#{url[1..-2]}\" #{opts.join ' '} />"
end.join "\n"
end
def self.registered(base)
def self.registered(_base)
puts "WARNING: #{self} is a helpers module, not an extension."
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'mustermann'
module Sinatra
# = Sinatra::Namespace
#
# <tt>Sinatra::Namespace</tt> is an extension that adds namespaces to an
@ -187,13 +188,16 @@ module Sinatra
module Namespace
def self.new(base, pattern, conditions = {}, &block)
Module.new do
#quelch uninitialized variable warnings, since these get used by compile method.
@pattern, @conditions = nil, nil
# quelch uninitialized variable warnings, since these get used by compile method.
@pattern = nil
@conditions = nil
extend NamespacedMethods
include InstanceMethods
@base, @extensions, @errors = base, [], {}
@base = base
@extensions = []
@errors = {}
@pattern, @conditions = compile(pattern, conditions)
@templates = Hash.new { |h,k| @base.templates[k] }
@templates = Hash.new { |_h, k| @base.templates[k] }
namespace = self
before { extend(@namespace = namespace) }
class_eval(&block)
@ -224,14 +228,14 @@ module Sinatra
include SharedMethods
attr_reader :base, :templates
ALLOWED_ENGINES = [
:erb, :erubi, :haml, :hamlit, :builder, :nokogiri,
:liquid, :markdown, :rdoc, :asciidoc, :markaby,
:rabl, :slim, :yajl
ALLOWED_ENGINES = %i[
erb erubi haml hamlit builder nokogiri
liquid markdown rdoc asciidoc markaby
rabl slim yajl
]
def self.prefixed(*names)
names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) }}
names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) } }
end
prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put
@ -267,7 +271,7 @@ module Sinatra
end
def error(*codes, &block)
args = Sinatra::Base.send(:compile!, "ERROR", /.*/, block)
args = Sinatra::Base.send(:compile!, 'ERROR', /.*/, block)
codes = codes.map { |c| Array(c) }.flatten
codes << Exception if codes.empty?
codes << Sinatra::NotFound if codes.include?(404)
@ -280,12 +284,14 @@ module Sinatra
def respond_to(*args)
return @conditions[:provides] || base.respond_to if args.empty?
@conditions[:provides] = args
end
def set(key, value = self, &block)
return key.each { |k,v| set(k, v) } if key.respond_to?(:each) and block.nil? and value == self
return key.each { |k, v| set(k, v) } if key.respond_to?(:each) && block.nil? && (value == self)
raise ArgumentError, "may not set #{key}" unless ([:views] + ALLOWED_ENGINES).include?(key)
block ||= proc { value }
singleton_class.send(:define_method, key, &block)
end
@ -300,11 +306,12 @@ module Sinatra
def template(name, &block)
first_location = caller_locations.first
filename, line = first_location.path, first_location.lineno
filename = first_location.path
line = first_location.lineno
templates[name] = [block, filename, line]
end
def layout(name=:layout, &block)
def layout(name = :layout, &block)
template name, &block
end
@ -323,21 +330,22 @@ module Sinatra
conditions = conditions.merge pattern.to_hash
pattern = nil
end
base_pattern, base_conditions = @pattern, @conditions
base_pattern = @pattern
base_conditions = @conditions
pattern ||= default_pattern
[ prefixed_path(base_pattern, pattern),
(base_conditions || {}).merge(conditions) ]
[prefixed_path(base_pattern, pattern),
(base_conditions || {}).merge(conditions)]
end
def prefixed_path(a, b)
return a || b || /.*/ unless a and b
return a || b || /.*/ unless a && b
return Mustermann.new(b) if a == /.*/
Mustermann.new(a) + Mustermann.new(b)
end
def prefixed(method, pattern = nil, conditions = {}, &block)
default = %r{(?:/.*)?} if method == :before or method == :after
default = %r{(?:/.*)?} if (method == :before) || (method == :after)
pattern, conditions = compile pattern, conditions, default
result = base.send(method, pattern, **conditions, &block)
invoke_hook :route_added, method.to_s.upcase, pattern, block

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Sinatra
# = Sinatra::QuietLogger
#
@ -32,10 +34,14 @@ module Sinatra
# end
#
module QuietLogger
def self.registered(app)
quiet_logger_prefixes = app.settings.quiet_logger_prefixes.join('|') rescue ''
quiet_logger_prefixes = begin
app.settings.quiet_logger_prefixes.join('|')
rescue StandardError
''
end
return warn('You need to specify the paths you wish to exclude from logging via `set :quiet_logger_prefixes, %w(images css fonts)`') if quiet_logger_prefixes.empty?
const_set('QUIET_LOGGER_REGEX', %r(\A/{0,2}(?:#{quiet_logger_prefixes})))
::Rack::CommonLogger.prepend(
::Module.new do
@ -45,6 +51,5 @@ module Sinatra
end
)
end
end
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::Reloader
#
# Extension to reload modified files. Useful during development,
@ -94,11 +95,9 @@ module Sinatra
# end
#
module Reloader
# Watches a file so it can tell when it has been updated, and what
# elements does it contain.
class Watcher
# Represents an element of a Sinatra application that may need to
# be reloaded. An element could be:
# * a route
@ -172,7 +171,8 @@ module Sinatra
# Creates a new +Watcher+ instance for the file located at +path+.
def initialize(path)
@ignore = nil
@path, @elements = path, []
@path = path
@elements = []
update
end
@ -254,12 +254,13 @@ module Sinatra
reloaded_paths << watcher.path
end
return if reloaded_paths.empty?
@@after_reload.each do |block|
block.arity != 0 ? block.call(reloaded_paths) : block.call
block.arity.zero? ? block.call : block.call(reloaded_paths)
end
# Prevents after_reload from increasing each time it's reloaded.
@@after_reload.delete_if do |blk|
path, _ = blk.source_location
path, = blk.source_location
path && reloaded_paths.include?(path)
end
end
@ -286,7 +287,7 @@ module Sinatra
block.source_location.first : caller_files[1]
signature = super
watch_element(
source_location, :route, { :verb => verb, :signature => signature }
source_location, :route, { verb: verb, signature: signature }
)
signature
end
@ -295,9 +296,8 @@ module Sinatra
# tells the +Watcher::List+ for the Sinatra application to watch the
# inline templates in +file+ or the file who made the call to this
# method.
def inline_templates=(file=nil)
file = (file.nil? || file == true) ?
(caller_files[1] || File.expand_path($0)) : file
def inline_templates=(file = nil)
file = (caller_files[1] || File.expand_path($0)) if file.nil? || file == true
watch_element(file, :inline_templates)
super
end
@ -329,7 +329,7 @@ module Sinatra
path = caller_files[1] || File.expand_path($0)
result = super
codes.each do |c|
watch_element(path, :error, :code => c, :handler => @errors[c])
watch_element(path, :error, code: c, handler: @errors[c])
end
result
end
@ -358,17 +358,17 @@ module Sinatra
# Removes the +element+ from the Sinatra application.
def deactivate(element)
case element.type
when :route then
when :route
verb = element.representation[:verb]
signature = element.representation[:signature]
(routes[verb] ||= []).delete(signature)
when :middleware then
when :middleware
@middleware.delete(element.representation)
when :before_filter then
when :before_filter
filters[:before].delete(element.representation)
when :after_filter then
when :after_filter
filters[:after].delete(element.representation)
when :error then
when :error
code = element.representation[:code]
handler = element.representation[:handler]
@errors.delete(code) if @errors[code] == handler
@ -387,7 +387,7 @@ module Sinatra
Dir[*glob].each { |path| Watcher::List.for(self).ignore(path) }
end
private
private
# attr_reader :register_path warn on -w (private attribute)
def register_path; @register_path ||= nil; end
@ -415,7 +415,7 @@ module Sinatra
# watch it in the file where the extension has been registered.
# This prevents the duplication of the elements added by the
# extension in its +registered+ method with every reload.
def watch_element(path, type, representation=nil)
def watch_element(path, type, representation = nil)
list = Watcher::List.for(self)
element = Watcher::Element.new(type, representation)
list.watch(path, element)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
@ -60,7 +62,7 @@ module Sinatra
elsif key.is_a?(Array)
_required_params(p, *key)
else
halt 400 unless p && p.respond_to?(:has_key?) && p.has_key?(key.to_s)
halt 400 unless p.respond_to?(:key?) && p&.key?(key.to_s)
end
end
true

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/json'
require 'sinatra/base'
@ -88,14 +90,17 @@ module Sinatra
module RespondWith
class Format
def initialize(app)
@app, @map, @generic, @default = app, {}, {}, nil
@app = app
@map = {}
@generic = {}
@default = nil
end
def on(type, &block)
@app.settings.mime_types(type).each do |mime|
case mime
when '*/*' then @default = block
when /^([^\/]+)\/\*$/ then @generic[$1] = block
when %r{^([^/]+)/\*$} then @generic[$1] = block
else @map[mime] = block
end
end
@ -103,23 +108,24 @@ module Sinatra
def finish
yield self if block_given?
mime_type = @app.content_type ||
@app.request.preferred_type(@map.keys) ||
@app.request.preferred_type ||
'text/html'
mime_type = @app.content_type ||
@app.request.preferred_type(@map.keys) ||
@app.request.preferred_type ||
'text/html'
type = mime_type.split(/\s*;\s*/, 2).first
handlers = [@map[type], @generic[type[/^[^\/]+/]], @default].compact
handlers = [@map[type], @generic[type[%r{^[^/]+}]], @default].compact
handlers.each do |block|
if result = block.call(type)
if (result = block.call(type))
@app.content_type mime_type
@app.halt result
end
end
@app.halt 500, "Unknown template engine"
@app.halt 500, 'Unknown template engine'
end
def method_missing(method, *args, &block)
return super if args.any? or block.nil? or not @app.mime_type(method)
return super if args.any? || block.nil? || !@app.mime_type(method)
on(method, &block)
end
end
@ -128,19 +134,22 @@ module Sinatra
include Sinatra::JSON
def respond_with(template, object = nil, &block)
object, template = template, nil unless Symbol === template
unless Symbol === template
object = template
template = nil
end
format = Format.new(self)
format.on "*/*" do |type|
format.on '*/*' do |type|
exts = settings.ext_map[type]
exts << :xml if type.end_with? '+xml'
if template
args = template_cache.fetch(type, template) { template_for(template, exts) }
if args.any?
locals = { :object => object }
locals = { object: object }
locals.merge! object.to_hash if object.respond_to? :to_hash
renderer = args.first
options = args[1..-1] + [{:locals => locals}]
options = args[1..] + [{ locals: locals }]
halt send(renderer, *options)
end
@ -149,6 +158,7 @@ module Sinatra
exts.each do |ext|
halt json(object) if ext == :json
next unless object.respond_to? method = "to_#{ext}"
halt(*object.send(method))
end
end
@ -176,10 +186,11 @@ module Sinatra
possible.each do |engine, template|
klass = Tilt.default_mapping.template_map[engine.to_s] ||
Tilt.lazy_map[engine.to_s].fetch(0, [])[0]
Tilt.lazy_map[engine.to_s].fetch(0, [])[0]
find_template(settings.views, template, klass) do |file|
next unless File.exist? file
return settings.rendering_method(engine) << template.to_sym
end
end
@ -189,7 +200,7 @@ module Sinatra
def remap_extensions
ext_map.clear
Rack::Mime::MIME_TYPES.each { |e,t| ext_map[t] << e[1..-1].to_sym }
Rack::Mime::MIME_TYPES.each { |e, t| ext_map[t] << e[1..].to_sym }
ext_map['text/javascript'] << 'js'
ext_map['text/xml'] << 'xml'
end
@ -206,7 +217,7 @@ module Sinatra
if formats.any?
@respond_to ||= []
@respond_to.concat formats
elsif @respond_to.nil? and superclass.respond_to? :respond_to
elsif @respond_to.nil? && superclass.respond_to?(:respond_to)
superclass.respond_to
else
@respond_to
@ -216,7 +227,8 @@ module Sinatra
def rendering_method(engine)
return [engine] if Sinatra::Templates.method_defined? engine
return [:mab] if engine.to_sym == :markaby
[:render, :engine]
%i[render engine]
end
private
@ -228,8 +240,8 @@ module Sinatra
def self.jrubyify(engs)
not_supported = [:markdown]
engs.keys.each do |key|
engs[key].collect! { |eng| (eng == :yajl) ? :json_pure : eng }
engs.each_key do |key|
engs[key].collect! { |eng| eng == :yajl ? :json_pure : eng }
engs[key].delete_if { |eng| not_supported.include?(eng) }
end
engs
@ -237,19 +249,19 @@ module Sinatra
def self.engines
engines = {
:xml => [:builder, :nokogiri],
:html => [:erb, :erubi, :haml, :hamlit, :slim, :liquid,
:mab, :markdown, :rdoc],
:all => (Sinatra::Templates.instance_methods.map(&:to_sym) +
[:mab] - [:find_template, :markaby]),
:json => [:yajl],
xml: %i[builder nokogiri],
html: %i[erb erubi haml hamlit slim liquid
mab markdown rdoc],
all: (Sinatra::Templates.instance_methods.map(&:to_sym) +
[:mab] - %i[find_template markaby]),
json: [:yajl]
}
engines.default = []
(defined? JRUBY_VERSION) ? jrubyify(engines) : engines
defined?(JRUBY_VERSION) ? jrubyify(engines) : engines
end
def self.registered(base)
base.set :ext_map, Hash.new { |h,k| h[k] = [] }
base.set :ext_map, Hash.new { |h, k| h[k] = [] }
base.set :template_engines, engines
base.remap_extensions
base.helpers Helpers

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'open-uri'
require 'net/http'
require 'timeout'
@ -49,7 +51,7 @@ module Sinatra
# For an example, check https://github.com/apotonick/roar/blob/master/test/integration/runner.rb
class Runner
def app_file
File.expand_path("server.rb", __dir__)
File.expand_path('server.rb', __dir__)
end
def run
@ -60,7 +62,8 @@ module Sinatra
def kill
return unless pipe
Process.kill("KILL", pipe.pid)
Process.kill('KILL', pipe.pid)
rescue NotImplementedError
system "kill -9 #{pipe.pid}"
rescue Errno::ESRCH
@ -70,7 +73,7 @@ module Sinatra
Timeout.timeout(1) { get_url("#{protocol}://127.0.0.1:#{port}#{url}") }
end
def get_stream(url = "/stream", &block)
def get_stream(url = '/stream', &block)
Net::HTTP.start '127.0.0.1', port do |http|
request = Net::HTTP::Get.new url
http.request request do |response|
@ -89,29 +92,32 @@ module Sinatra
end
def log
@log ||= ""
loop { @log << pipe.read_nonblock(1) }
@log ||= ''
loop { @log << pipe.read_nonblock(1) }
rescue Exception
@log
end
private
private
attr_accessor :pipe
def start
IO.popen(command)
end
def command # to be overwritten
# to be overwritten
def command
"bundle exec ruby #{app_file} -p #{port} -e production"
end
def ping(timeout=30)
def ping(timeout = 30)
loop do
return if alive?
if Time.now - @started > timeout
$stderr.puts command, log
fail "timeout"
warn command, log
raise 'timeout'
else
sleep 0.1
end
@ -121,26 +127,29 @@ module Sinatra
def alive?
3.times { get(ping_path) }
true
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, EOFError, SystemCallError, OpenURI::HTTPError, Timeout::Error
rescue EOFError, SystemCallError, OpenURI::HTTPError, Timeout::Error
false
end
def ping_path # to be overwritten
# to be overwritten
def ping_path
'/ping'
end
def port # to be overwritten
# to be overwritten
def port
4567
end
def protocol
"http"
'http'
end
def get_url(url)
uri = URI.parse(url)
return uri.read unless protocol == "https"
return uri.read unless protocol == 'https'
get_https_url(uri)
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::Streaming
#
# Sinatra 1.3 introduced the +stream+ helper. This addon improves the
@ -84,13 +85,14 @@ module Sinatra
end
module Stream
attr_accessor :app, :lineno, :pos, :transformer, :closed
alias tell pos
alias closed? closed
def self.extended(obj)
obj.closed, obj.lineno, obj.pos = false, 0, 0
obj.closed = false
obj.lineno = 0
obj.pos = 0
obj.callback { obj.closed = true }
obj.errback { obj.closed = true }
end
@ -108,6 +110,7 @@ module Sinatra
def each
# that way body.each.map { ... } works
return self unless block_given?
super
end
@ -120,7 +123,8 @@ module Sinatra
@transformer ||= nil
if @transformer
inner, outer = @transformer, block
inner = @transformer
outer = block
block = proc { |value| outer[inner[value]] }
end
@transformer = block
@ -132,7 +136,7 @@ module Sinatra
data.to_s.bytesize
end
alias syswrite write
alias syswrite write
alias write_nonblock write
def print(*args)
@ -154,7 +158,7 @@ module Sinatra
end
def close_read
raise IOError, "closing non-duplex IO for reading"
raise IOError, 'closing non-duplex IO for reading'
end
def closed_read?
@ -171,10 +175,6 @@ module Sinatra
settings.default_encoding
end
def closed?
@closed
end
def settings
app.settings
end
@ -184,7 +184,7 @@ module Sinatra
end
def not_open_for_reading(*)
raise IOError, "not opened for reading"
raise IOError, 'not opened for reading'
end
alias bytes not_open_for_reading

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'sinatra/base'
require 'rack'
begin
@ -158,7 +160,7 @@ module Sinatra
# @param params [Hash]
# @param env [Hash]
def options(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
env = env_for(uri, env.merge(method: 'OPTIONS', params: params))
current_session.send(:process_request, uri, env, &block)
end
end
@ -170,7 +172,7 @@ module Sinatra
# @param params [Hash]
# @param env [Hash]
def patch(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(:method => "PATCH", :params => params))
env = env_for(uri, env.merge(method: 'PATCH', params: params))
current_session.send(:process_request, uri, env, &block)
end
end
@ -187,7 +189,8 @@ module Sinatra
# @return [Hash] Session of last request, or the empty Hash
def session
return {} unless last_request?
raise Rack::Test::Error, "session not enabled for app" unless last_env["rack.session"] or app.session?
raise Rack::Test::Error, 'session not enabled for app' unless last_env['rack.session'] || app.session?
last_request.session
end

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'sinatra/base'
module Sinatra
# = Sinatra::WebDAV
#
# This extensions provides WebDAV verbs, as defined by RFC 4918
@ -37,8 +38,8 @@ module Sinatra
module Request
def self.included(base)
base.class_eval do
alias _safe? safe?
alias _idempotent? idempotent?
alias_method :_safe?, :safe?
alias_method :_idempotent?, :idempotent?
def safe?
_safe? or propfind?
@ -70,9 +71,9 @@ module Sinatra
request_method == 'MOVE'
end
#def lock?
# def lock?
# request_method == 'LOCK'
#end
# end
def unlock?
request_method == 'UNLOCK'
@ -84,7 +85,7 @@ module Sinatra
def mkcol(path, opts = {}, &bk) route 'MKCOL', path, opts, &bk end
def copy(path, opts = {}, &bk) route 'COPY', path, opts, &bk end
def move(path, opts = {}, &bk) route 'MOVE', path, opts, &bk end
#def lock(path, opts = {}, &bk) route 'LOCK', path, opts, &bk end
# def lock(path, opts = {}, &bk) route 'LOCK', path, opts, &bk end
def unlock(path, opts = {}, &bk) route 'UNLOCK', path, opts, &bk end
end

View File

@ -1,57 +1,58 @@
# -*- encoding: utf-8 -*-
# frozen_string_literal: true
version = File.read(File.expand_path("../VERSION", __dir__)).strip
version = File.read(File.expand_path('../VERSION', __dir__)).strip
Gem::Specification.new do |s|
s.name = "sinatra-contrib"
s.name = 'sinatra-contrib'
s.version = version
s.description = "Collection of useful Sinatra extensions"
s.homepage = "http://sinatrarb.com/contrib/"
s.license = "MIT"
s.description = 'Collection of useful Sinatra extensions'
s.homepage = 'http://sinatrarb.com/contrib/'
s.license = 'MIT'
s.summary = s.description
s.authors = ["https://github.com/sinatra/sinatra/graphs/contributors"]
s.email = "sinatrarb@googlegroups.com"
s.files = Dir["lib/**/*.rb"] + [
"LICENSE",
"README.md",
"Rakefile",
"ideas.md",
"sinatra-contrib.gemspec"
s.authors = ['https://github.com/sinatra/sinatra/graphs/contributors']
s.email = 'sinatrarb@googlegroups.com'
s.files = Dir['lib/**/*.rb'] + [
'LICENSE',
'README.md',
'Rakefile',
'ideas.md',
'sinatra-contrib.gemspec'
]
if s.respond_to?(:metadata)
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/sinatra-contrib',
'homepage_uri' => 'http://sinatrarb.com/contrib/',
'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra-contrib'
}
else
raise <<-EOF
unless s.respond_to?(:metadata)
raise <<-WARN
RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running:
gem install rubygems-update
update_rubygems:
gem update --system
EOF
WARN
end
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/sinatra-contrib',
'homepage_uri' => 'http://sinatrarb.com/contrib/',
'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra-contrib',
'rubygems_mfa_required' => 'true'
}
s.required_ruby_version = '>= 2.6.0'
s.add_dependency "sinatra", version
s.add_dependency "mustermann", "~> 3.0"
s.add_dependency "tilt", "~> 2.0"
s.add_dependency "rack-protection", version
s.add_dependency "multi_json"
s.add_dependency 'multi_json'
s.add_dependency 'mustermann', '~> 3.0'
s.add_dependency 'rack-protection', version
s.add_dependency 'sinatra', version
s.add_dependency 'tilt', '~> 2.0'
s.add_development_dependency "rspec", "~> 3"
s.add_development_dependency "haml"
s.add_development_dependency "erubi"
s.add_development_dependency "slim"
s.add_development_dependency "builder"
s.add_development_dependency "liquid"
s.add_development_dependency "redcarpet"
s.add_development_dependency "asciidoctor"
s.add_development_dependency "nokogiri"
s.add_development_dependency "markaby"
s.add_development_dependency "rake", "< 11"
s.add_development_dependency "rack-test", "~> 2"
s.add_development_dependency 'asciidoctor'
s.add_development_dependency 'builder'
s.add_development_dependency 'erubi'
s.add_development_dependency 'haml'
s.add_development_dependency 'liquid'
s.add_development_dependency 'markaby'
s.add_development_dependency 'nokogiri'
s.add_development_dependency 'rack-test', '~> 2'
s.add_development_dependency 'rake', '< 11'
s.add_development_dependency 'redcarpet'
s.add_development_dependency 'rspec', '~> 3'
s.add_development_dependency 'slim'
end

View File

@ -1,14 +1,14 @@
require 'spec_helper'
RSpec.describe Sinatra::Cookies do
def cookie_route(*cookies, &block)
def cookie_route(*cookies, headers: {}, &block)
result = nil
set_cookie(cookies)
@cookie_app.get('/') do
result = instance_eval(&block)
"ok"
end
get '/', {}, @headers || {}
get '/', {}, headers || {}
expect(last_response).to be_ok
expect(body).to eq("ok")
result
@ -97,8 +97,8 @@ RSpec.describe Sinatra::Cookies do
end
it 'sets domain to nil if localhost' do
@headers = {'HTTP_HOST' => 'localhost'}
expect(cookie_route do
headers = {'HTTP_HOST' => 'localhost'}
expect(cookie_route(headers: headers) do
cookies['foo'] = 'bar'
response['Set-Cookie']
end).not_to include("domain")

View File

@ -263,6 +263,7 @@ RSpec.describe Sinatra::Namespace do
specify 'are accepted in the before-filter' do
namespace '/foo' do
before { @yes = nil }
before(:host_name => 'example.com') { @yes = 'yes' }
send(verb) { @yes || 'no' }
end

View File

@ -1,51 +1,54 @@
version = File.read(File.expand_path("VERSION", __dir__)).strip
# frozen_string_literal: true
version = File.read(File.expand_path('VERSION', __dir__)).strip
Gem::Specification.new 'sinatra', version do |s|
s.description = "Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort."
s.summary = "Classy web-development dressed in a DSL"
s.authors = ["Blake Mizerany", "Ryan Tomayko", "Simon Rozet", "Konstantin Haase"]
s.email = "sinatrarb@googlegroups.com"
s.homepage = "http://sinatrarb.com/"
s.description = 'Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort.'
s.summary = 'Classy web-development dressed in a DSL'
s.authors = ['Blake Mizerany', 'Ryan Tomayko', 'Simon Rozet', 'Konstantin Haase']
s.email = 'sinatrarb@googlegroups.com'
s.homepage = 'http://sinatrarb.com/'
s.license = 'MIT'
s.files = Dir['README*.md', 'lib/**/*', 'examples/*'] + [
".yardopts",
"AUTHORS.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"Gemfile",
"LICENSE",
"MAINTENANCE.md",
"Rakefile",
"SECURITY.md",
"sinatra.gemspec",
"VERSION"]
'.yardopts',
'AUTHORS.md',
'CHANGELOG.md',
'CONTRIBUTING.md',
'Gemfile',
'LICENSE',
'MAINTENANCE.md',
'Rakefile',
'SECURITY.md',
'sinatra.gemspec',
'VERSION'
]
s.extra_rdoc_files = %w[README.md LICENSE]
s.rdoc_options = %w[--line-numbers --title Sinatra --main README.rdoc --encoding=UTF-8]
if s.respond_to?(:metadata)
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra',
'changelog_uri' => 'https://github.com/sinatra/sinatra/blob/master/CHANGELOG.md',
'homepage_uri' => 'http://sinatrarb.com/',
'bug_tracker_uri' => 'https://github.com/sinatra/sinatra/issues',
'mailing_list_uri' => 'http://groups.google.com/group/sinatrarb',
'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra'
}
else
raise <<-EOF
unless s.respond_to?(:metadata)
raise <<-WARN
RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running:
gem install rubygems-update
update_rubygems:
gem update --system
EOF
WARN
end
s.metadata = {
'source_code_uri' => 'https://github.com/sinatra/sinatra',
'changelog_uri' => 'https://github.com/sinatra/sinatra/blob/master/CHANGELOG.md',
'homepage_uri' => 'http://sinatrarb.com/',
'bug_tracker_uri' => 'https://github.com/sinatra/sinatra/issues',
'mailing_list_uri' => 'http://groups.google.com/group/sinatrarb',
'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra'
}
s.required_ruby_version = '>= 2.6.0'
s.add_dependency 'rack', '~> 2.2', '>= 2.2.4'
s.add_dependency 'tilt', '~> 2.0'
s.add_dependency 'rack-protection', version
s.add_dependency 'mustermann', '~> 3.0'
s.add_dependency 'rack', '~> 2.2', '>= 2.2.4'
s.add_dependency 'rack-protection', version
s.add_dependency 'tilt', '~> 2.0'
s.add_development_dependency 'rack-test', '~> 2'
end