Class: Cuba

Inherits:
Object
  • Object
show all
Defined in:
lib/cuba/safe/secure_headers.rb,
lib/cuba.rb,
lib/cuba/safe.rb,
lib/cuba/render.rb,
lib/cuba/safe/csrf.rb

Overview

Secure HTTP Headers

This plugin will automatically apply several headers that are related to security. This includes:

- HTTP Strict Transport Security (HSTS) [2].
- X-Frame-Options [3].
- X-XSS-Protection [4].
- X-Content-Type-Options [5].
- X-Download-Options [6].
- X-Permitted-Cross-Domain-Policies [7].

Due to HTTP/2 specifications and Rack specifications, field names are applied in all lowercase.

References

[1]: github.com/twitter/secureheaders [2]: tools.ietf.org/html/rfc6797 [3]: tools.ietf.org/html/draft-ietf-websec-x-frame-options-02 [4]: msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx [5]: msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx [6]: msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx [7]: www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html [8]: datatracker.ietf.org/doc/html/rfc9113#name-http-fields

Defined Under Namespace

Modules: Render, Safe Classes: Response

Constant Summary collapse

SLASH =
"/".freeze
EMPTY =
"".freeze
SEGMENT =
"([^\\/]+)".freeze
DEFAULT =
"text/html; charset=utf-8".freeze
REGEXES =
Hash.new { |h, pattern| h[pattern] = /\A\/(#{pattern})(\/|\z)/ }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&blk) ⇒ Cuba

Returns a new instance of Cuba.



134
135
136
137
# File 'lib/cuba.rb', line 134

def initialize(&blk)
  @blk = blk
  @captures = []
end

Instance Attribute Details

#capturesObject (readonly)

Returns the value of attribute captures.



132
133
134
# File 'lib/cuba.rb', line 132

def captures
  @captures
end

#envObject (readonly)

Returns the value of attribute env.



129
130
131
# File 'lib/cuba.rb', line 129

def env
  @env
end

#reqObject (readonly)

Returns the value of attribute req.



130
131
132
# File 'lib/cuba.rb', line 130

def req
  @req
end

#resObject (readonly)

Returns the value of attribute res.



131
132
133
# File 'lib/cuba.rb', line 131

def res
  @res
end

Class Method Details

.appObject



90
91
92
# File 'lib/cuba.rb', line 90

def self.app
  @app ||= Rack::Builder.new
end

.call(env) ⇒ Object



106
107
108
# File 'lib/cuba.rb', line 106

def self.call(env)
  prototype.call(env)
end

.deepclone(obj) ⇒ Object



121
122
123
# File 'lib/cuba.rb', line 121

def self.deepclone(obj)
  Marshal.load(Marshal.dump(obj))
end

.define(&block) ⇒ Object



98
99
100
# File 'lib/cuba.rb', line 98

def self.define(&block)
  app.run new(&block)
end

.inherited(child) ⇒ Object



125
126
127
# File 'lib/cuba.rb', line 125

def self.inherited(child)
  child.settings.replace(deepclone(settings))
end

.plugin(mixin) ⇒ Object



110
111
112
113
114
115
# File 'lib/cuba.rb', line 110

def self.plugin(mixin)
  include mixin
  extend  mixin::ClassMethods if defined?(mixin::ClassMethods)

  mixin.setup(self) if mixin.respond_to?(:setup)
end

.prototypeObject



102
103
104
# File 'lib/cuba.rb', line 102

def self.prototype
  @prototype ||= app.to_app
end

.reset!Object



85
86
87
88
# File 'lib/cuba.rb', line 85

def self.reset!
  @app = nil
  @prototype = nil
end

.settingsObject



117
118
119
# File 'lib/cuba.rb', line 117

def self.settings
  @settings ||= {}
end

.use(middleware, *args, **kwargs, &block) ⇒ Object



94
95
96
# File 'lib/cuba.rb', line 94

def self.use(middleware, *args, **kwargs, &block)
  app.use(middleware, *args, **kwargs, &block)
end

Instance Method Details

#accept(mimetype) ⇒ Object

If you want to match against the HTTP_ACCEPT value.

Examples:

# HTTP_ACCEPT=application/xml
on accept("application/xml") do
  # automatically set to application/xml.
  res.write res["Content-Type"]
end


314
315
316
317
318
319
320
321
322
# File 'lib/cuba.rb', line 314

def accept(mimetype)
  lambda do
    accept = String(env["HTTP_ACCEPT"]).split(",")

    if accept.any? { |s| s.strip == mimetype }
      res[Rack::CONTENT_TYPE] = mimetype
    end
  end
end

#call(env) ⇒ Object



143
144
145
# File 'lib/cuba.rb', line 143

def call(env)
  dup.call!(env)
end

#call!(env) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/cuba.rb', line 147

def call!(env)
  @env = env
  @req = settings[:req].new(env)
  @res = settings[:res].new(settings[:default_headers].dup)

  # This `catch` statement will either receive a
  # rack response tuple via a `halt`, or will
  # fall back to issuing a 404.
  #
  # When it `catch`es a throw, the return value
  # of this whole `call!` method will be the
  # rack response tuple, which is exactly what we want.
  catch(:halt) do
    instance_eval(&@blk)

    not_found
    res.finish
  end
end

#defaultObject

Syntactic sugar for providing catch-all matches.

Examples:

on default do
  res.write "404"
end


330
331
332
# File 'lib/cuba.rb', line 330

def default
  true
end

#deleteObject



358
# File 'lib/cuba.rb', line 358

def delete;  req.delete?  end

#extension(ext = "\\w+") ⇒ Object

A matcher for files with a certain extension.

Examples:

# PATH_INFO=/style/app.css
on "style", extension("css") do |file|
  res.write file # writes app
end


272
273
274
# File 'lib/cuba.rb', line 272

def extension(ext = "\\w+")
  lambda { consume("([^\\/]+?)\.#{ext}\\z") }
end

#getObject

Syntatic sugar for providing HTTP Verb matching.

Examples:

on get, "signup" do
end

on post, "signup" do
end


354
# File 'lib/cuba.rb', line 354

def get;     req.get?     end

#halt(response) ⇒ Object



382
383
384
# File 'lib/cuba.rb', line 382

def halt(response)
  throw :halt, response
end

#headObject



359
# File 'lib/cuba.rb', line 359

def head;    req.head?    end

#host(hostname) ⇒ Object

Useful for matching against the request host (i.e. HTTP_HOST).

Examples:

on host("account1.example.com"), "api" do
  res.write "You have reached the API of account1."
end


302
303
304
# File 'lib/cuba.rb', line 302

def host(hostname)
  hostname === req.host
end


361
# File 'lib/cuba.rb', line 361

def link;    req.link?    end

#match(matcher, segment = SEGMENT) ⇒ Object



254
255
256
257
258
259
260
261
262
263
# File 'lib/cuba.rb', line 254

def match(matcher, segment = SEGMENT)
  case matcher
  when String then consume(matcher.gsub(/:\w+/, segment))
  when Regexp then consume(matcher)
  when Symbol then consume(segment)
  when Proc   then matcher.call
  else
    matcher
  end
end

#not_foundObject



429
430
431
# File 'lib/cuba.rb', line 429

def not_found
  res.status = 404
end

#on(*args, &block) ⇒ Object

The heart of the path / verb / any condition matching.

Examples:


on get do
  res.write "GET"
end

on get, "signup" do
  res.write "Signup"
end

on "user/:id" do |uid|
  res.write "User: #{uid}"
end

on "styles", extension("css") do |file|
  res.write render("styles/#{file}.sass")
end


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/cuba.rb', line 193

def on(*args, &block)
  try do
    # For every block, we make sure to reset captures so that
    # nesting matchers won't mess with each other's captures.
    @captures = []

    # We stop evaluation of this entire matcher unless
    # each and every `arg` defined for this matcher evaluates
    # to a non-false value.
    #
    # Short circuit examples:
    #    on true, false do
    #
    #    # PATH_INFO=/user
    #    on true, "signup"
    return unless args.all? { |arg| match(arg) }

    # The captures we yield here were generated and assembled
    # by evaluating each of the `arg`s above. Most of these
    # are carried out by #consume.
    yield(*captures)

    if res.status.nil?
      if res.body.empty?
        not_found
      else
        res.headers[Rack::CONTENT_TYPE] ||= DEFAULT
        res.status = 200
      end
    end

    halt(res.finish)
  end
end

#optionsObject



360
# File 'lib/cuba.rb', line 360

def options; req.options? end

#param(key, default = nil) ⇒ Object

Ensures that certain request parameters are present. Acts like a precondition / assertion for your route. A default value can be provided as a second argument. In that case, it always matches and the result is either the parameter or the default value.

Examples:

# POST with data like user[fname]=John&user[lname]=Doe
on "signup", param("user") do |atts|
  User.create(atts)
end

on "login", param("username", "guest") do |username|
  # If not provided, username == "guest"
end


290
291
292
293
294
# File 'lib/cuba.rb', line 290

def param(key, default = nil)
  value = req.params[key.to_s] || default

  lambda { captures << value unless value.to_s.empty? }
end

#patchObject



357
# File 'lib/cuba.rb', line 357

def patch;   req.patch?   end

#postObject



355
# File 'lib/cuba.rb', line 355

def post;    req.post?    end

#putObject



356
# File 'lib/cuba.rb', line 356

def put;     req.put?     end

#rootObject

Access the root of the application.

Examples:


# GET /
on root do
  res.write "Home"
end


342
343
344
# File 'lib/cuba.rb', line 342

def root
  env[Rack::PATH_INFO] == SLASH || env[Rack::PATH_INFO] == EMPTY
end

#run(app) ⇒ Object

If you want to halt the processing of an existing handler and continue it via a different handler.

Examples:

def redirect(*args)
  run Cuba.new { on(default) { res.redirect(*args) }}
end

on "account" do
  redirect "/login" unless session["uid"]

  res.write "Super secure account info."
end


378
379
380
# File 'lib/cuba.rb', line 378

def run(app)
  halt app.call(req.env)
end

#sessionObject



167
168
169
170
171
# File 'lib/cuba.rb', line 167

def session
  env["rack.session"] || raise(RuntimeError,
    "You're missing a session handler. You can get started " +
    "by adding Cuba.use Rack::Session::Cookie")
end

#settingsObject



139
140
141
# File 'lib/cuba.rb', line 139

def settings
  self.class.settings
end

#traceObject



363
# File 'lib/cuba.rb', line 363

def trace;   req.trace?   end


362
# File 'lib/cuba.rb', line 362

def unlink;  req.unlink?  end

#varsObject

Returns a hash with the information set by the #with method.

with(role: "admin", site: "main") do
  on default do
    res.write(vars.inspect)
  end
end
# => '{:role=>"admin", :site=>"main"}'


425
426
427
# File 'lib/cuba.rb', line 425

def vars
  env["cuba.vars"] ||= {}
end

#with(dict = {}) ⇒ Object

Adds ability to pass information to a nested Cuba application. It receives two parameters: a hash that represents the passed information and a block. The #vars method is used to retrieve a hash with the passed information.

class Platforms < Cuba
  define do
    platform = vars[:platform]

    on default do
      res.write(platform) # => "heroku" or "salesforce"
    end
  end
end

Cuba.define do
  on "(heroku|salesforce)" do |platform|
    with(platform: platform) do
      run(Platforms)
    end
  end
end


409
410
411
412
413
414
# File 'lib/cuba.rb', line 409

def with(dict = {})
  old, env["cuba.vars"] = vars, vars.merge(dict)
  yield
ensure
  env["cuba.vars"] = old
end