Coder Social home page Coder Social logo

cuba's Introduction

Cuba

n. a microframework for web development.

Cuba and Rum, by Jan Sochor

Community

Meet us on IRC: #cuba.rb on freenode.net.

Description

Cuba is a microframework for web development originally inspired by Rum, a tiny but powerful mapper for Rack applications.

It integrates many templates via Tilt, and testing via Cutest and Capybara.

Installation

$ gem install cuba

Usage

Here's a simple application:

# cat hello_world.rb
require "cuba"
require "cuba/safe"

Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"

Cuba.plugin Cuba::Safe

Cuba.define do
  on get do
    on "hello" do
      res.write "Hello world!"
    end

    on root do
      res.redirect "/hello"
    end
  end
end

And the test file:

# cat hello_world_test.rb
require "cuba/test"
require "./hello_world"

scope do
  test "Homepage" do
    get "/"

    follow_redirect!

    assert_equal "Hello world!", last_response.body
  end
end

To run it, you can create a config.ru file:

# cat config.ru
require "./hello_world"

run Cuba

You can now run rackup and enjoy what you have just created.

Matchers

Here's an example showcasing how different matchers work:

require "cuba"
require "cuba/safe"

Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"

Cuba.plugin Cuba::Safe

Cuba.define do

  # only GET requests
  on get do

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

    # /about
    on "about" do
      res.write "About"
    end

    # /styles/basic.css
    on "styles", extension("css") do |file|
      res.write "Filename: #{file}" #=> "Filename: basic"
    end

    # /post/2011/02/16/hello
    on "post/:y/:m/:d/:slug" do |y, m, d, slug|
      res.write "#{y}-#{m}-#{d} #{slug}" #=> "2011-02-16 hello"
    end

    # /username/foobar
    on "username/:username" do |username|
      user = User.find_by_username(username) # username == "foobar"

      # /username/foobar/posts
      on "posts" do

        # You can access `user` here, because the `on` blocks
        # are closures.
        res.write "Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
      end

      # /username/foobar/following
      on "following" do
        res.write user.following.size #=> "1301"
      end
    end

    # /search?q=barbaz
    on "search", param("q") do |query|
      res.write "Searched for #{query}" #=> "Searched for barbaz"
    end
  end

  # only POST requests
  on post do
    on "login" do

      # POST /login, user: foo, pass: baz
      on param("user"), param("pass") do |user, pass|
        res.write "#{user}:#{pass}" #=> "foo:baz"
      end

      # If the params `user` and `pass` are not provided, this
      # block will get executed.
      on true do
        res.write "You need to provide user and pass!"
      end
    end
  end
end

Note that once an on block matches, processing halts at the conclusion of that block.

Status codes

If you don't assign a status code and you don't write to the res object, the status will be set as 404. The method not_found is in charge of setting the proper status code, and you can redefine it if you want to render a template or configure custom headers.

For example:

Cuba.define do
  on get do
    on "hello" do
      res.write "hello world"
    end
  end
end

# Requests:
#
# GET /            # 404
# GET /hello       # 200
# GET /hello/world # 200

As you can see, as soon as something was written to the response, the status code was changed to 200.

If you want to match just "hello", but not "hello/world", you can do as follows:

Cuba.define do
  on get do
    on "hello" do
      on root do
        res.write "hello world"
      end
    end
  end
end

# Requests:
#
# GET /            # 404
# GET /hello       # 200
# GET /hello/world # 404

You can also use a regular expression to match the end of line:

Cuba.define do
  on get do
    on /hello\/?\z/ do
      res.write "hello world"
    end
  end
end

# Requests:
#
# GET /            # 404
# GET /hello       # 200
# GET /hello/world # 404

This last example is not a common usage pattern. It's here only to illustrate how Cuba can be adapted for different use cases.

If you need this behavior, you can create a helper:

module TerminalMatcher
  def terminal(path)
    /#{path}\/?\z/
  end
end

Cuba.plugin TerminalMatcher

Cuba.define do
  on get do
    on terminal("hello") do
      res.write "hello world"
    end
  end
end

Security

The most important security consideration is to use https for all requests. If that's not the case, any attempt to secure the application could be in vain. The rest of this section assumes https is enforced.

When building a web application, you need to include a security layer. Cuba ships with the Cuba::Safe plugin, which applies several security related headers to prevent attacks like clickjacking and cross-site scripting, among others. It is not included by default because there are legitimate uses for plain Cuba (for instance, when designing an API).

Here's how to include it:

require "cuba/safe"

Cuba.plugin Cuba::Safe

You should also always set a session secret to some undisclosed value. Keep in mind that the content in the session cookie is not encrypted.

Cuba.use(Rack::Session::Cookie, :secret => "__a_very_long_string__")

In the end, your application should look like this:

require "cuba"
require "cuba/safe"

Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"

Cuba.plugin Cuba::Safe

Cuba.define do
  on csrf.unsafe? do
    csrf.reset!

    res.status = 403
    res.write("Not authorized")

    halt(res.finish)
  end

  # Now your app is protected against a wide range of attacks.
  ...
end

The Cuba::Safe plugin is composed of two modules:

  • Cuba::Safe::SecureHeaders
  • Cuba::Safe::CSRF

You can include them individually, but while the modularity is good for development, it's very common to use them in tandem. As that's the normal use case, including Cuba::Safe is the preferred way.

Cross-Site Request Forgery

The Cuba::Safe::CSRF plugin provides a csrf object with the following methods:

  • token: the current security token.
  • reset!: forces the token to be recreated.
  • safe?: returns true if the request is safe.
  • unsafe?: returns true if the request is unsafe.
  • form_tag: returns a string with the csrf_token hidden input tag.
  • meta_tag: returns a string with the csrf_token meta tag.

Here's an example of how to use it:

require "cuba"
require "cuba/safe"

Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"

Cuba.plugin Cuba::Safe

Cuba.define do
  on csrf.unsafe? do
    csrf.reset!

    res.status = 403
    res.write("Forbidden")

    halt(res.finish)
  end

  # Here comes the rest of your application
  # ...
end

You have to include csrf.form_tag in your forms and csrf.meta_tag among your meta tags. Here's an example that assumes you are using Cuba::Mote from cuba-contrib:

<!DOCTYPE html>
<html>
  <head>
    {{ app.csrf.meta_tag }}
    ...
  </head>
  ...
  <body>
    <form action="/foo" method="POST">
      {{ app.csrf.form_tag }}
      ...
    </form>
  ...
  </body>
</html>

HTTP Verbs

There are matchers defined for the following HTTP Verbs: get, post, put, patch, delete, head, options, link, unlink and trace. As you have the whole request available via the req object, you can also query it with helper methods like req.options? or req.head?, or you can even go to a lower level and inspect the environment via the env object, and check for example if env["REQUEST_METHOD"] equals the verb PATCH.

What follows is an example of different ways of saying the same thing:

on env["REQUEST_METHOD"] == "GET", "api" do ... end

on req.get?, "api" do ... end

on get, "api" do ... end

Actually, get is syntax sugar for req.get?, which in turn is syntax sugar for env["REQUEST_METHOD"] == "GET".

Headers

You can set the headers by assigning values to the hash req.headers. If you want to inspect the incoming headers, you have to read from the env hash. For example, if you want to know the referrer you can check env["HTTP_REFERER"].

Request and Response

You may have noticed we use req and res a lot. Those variables are instances of Rack::Request and Cuba::Response respectively, and Cuba::Response is just an optimized version of Rack::Response.

Those objects are helpers for accessing the request and for building the response. Most of the time, you will just use res.write.

If you want to use custom Request or Response objects, you can set the new values as follows:

Cuba.settings[:req] = MyRequest
Cuba.settings[:res] = MyResponse

Make sure to provide classes compatible with those from Rack.

Captures

You may have noticed that some matchers yield a value to the block. The rules for determining if a matcher will yield a value are simple:

  1. Regex captures: "posts/(\\d+)-(.*)" will yield two values, corresponding to each capture.
  2. Placeholders: "users/:id" will yield the value in the position of :id.
  3. Symbols: :foobar will yield if a segment is available.
  4. File extensions: extension("css") will yield the basename of the matched file.
  5. Parameters: param("user") will yield the value of the parameter user, if present.

The first case is important because it shows the underlying effect of regex captures.

In the second case, the substring :id gets replaced by ([^\\/]+) and the string becomes "users/([^\\/]+)" before performing the match, thus it reverts to the first form we saw.

In the third case, the symbol--no matter what it says--gets replaced by "([^\\/]+)", and again we are in presence of case 1.

The fourth case, again, reverts to the basic matcher: it generates the string "([^\\/]+?)\.#{ext}\\z" before performing the match.

The fifth case is different: it checks if the the parameter supplied is present in the request (via POST or QUERY_STRING) and it pushes the value as a capture.

Composition

You can mount a Cuba app, along with middlewares, inside another Cuba app:

class API < Cuba; end

API.use SomeMiddleware

API.define do
  on param("url") do |url|
    ...
  end
end

Cuba.define do
  on "api" do
    run API
  end
end

If you need to pass information to one sub-app, you can use the with method and access it with vars:

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

Embedding routes from other modules

While the run command allows you to handle over the control to a sub app, sometimes you may want to just embed routes defined in another module. There's no built-in method to do it, but if you are willing to experiment you can try the following.

Let's say you have defined routes in modules A and B, and you want to mount those routes in your application.

First, you will have to extend Cuba with this code:

class Cuba
  def mount(app)
    result = app.call(req.env)
    halt result if result[0] != 404
  end
end

It doesn't matter where you define it as long as Cuba has already been required. For instance, you could extract that to a plugin and it would work just fine.

Then, in your application, you can use it like this:

Cuba.define do
  on default do
    mount A 
    mount B
  end
end

It should halt the request only if the resulting status from calling the mounted app is not 404. If you run into some unexpected behavior, let me know by creating an issue and we'll look at how to workaround any difficulties.

Testing

Given that Cuba is essentially Rack, it is very easy to test with Rack::Test, Webrat or Capybara. Cuba's own tests are written with a combination of Cutest and Rack::Test, and if you want to use the same for your tests it is as easy as requiring cuba/test:

require "cuba/test"
require "your/app"

scope do
  test "Homepage" do
    get "/"

    assert_equal "Hello world!", last_response.body
  end
end

If you prefer to use Capybara, instead of requiring cuba/test you can require cuba/capybara:

require "cuba/capybara"
require "your/app"

scope do
  test "Homepage" do
    visit "/"

    assert has_content?("Hello world!")
  end
end

To read more about testing, check the documentation for Cutest, Rack::Test and Capybara.

Settings

Each Cuba app can store settings in the Cuba.settings hash. The settings are inherited if you happen to subclass Cuba

Cuba.settings[:layout] = "guest"

class Users < Cuba; end
class Admin < Cuba; end

Admin.settings[:layout] = "admin"

assert_equal "guest", Users.settings[:layout]
assert_equal "admin", Admin.settings[:layout]

Feel free to store whatever you find convenient.

Rendering

Cuba includes a plugin called Cuba::Render that provides a couple of helper methods for rendering templates. This plugin uses Tilt, which serves as an interface to a bunch of different Ruby template engines (ERB, Haml, Sass, CoffeeScript, etc.), so you can use the template engine of your choice.

To set up Cuba::Render, do:

require "cuba"
require "cuba/render"
require "erb"

Cuba.plugin Cuba::Render

This example uses ERB, a template engine that comes with Ruby. If you want to use another template engine, one supported by Tilt, you need to install the required gem and change the template_engine setting as shown below.

Cuba.settings[:render][:template_engine] = "haml"

The plugin provides three helper methods for rendering templates: partial, view and render.

Cuba.define do
  on "about" do
    # `partial` renders a template called `about.erb` without a layout.
    res.write partial("about")
  end

  on "home" do
    # Opposed to `partial`, `view` renders the same template
    # within a layout called `layout.erb`.
    res.write view("about")
  end

  on "contact" do
    # `render` is a shortcut to `res.write view(...)`
    render("contact")
  end
end

By default, Cuba::Render assumes that all templates are placed in a folder named views and that they use the proper extension for the chosen template engine. Also for the view and render methods, it assumes that the layout template is called layout.

The defaults can be changed through the Cuba.settings method:

Cuba.settings[:render][:template_engine] = "haml"
Cuba.settings[:render][:views] = "./views/admin/"
Cuba.settings[:render][:layout] = "admin"

NOTE: Cuba doesn't ship with Tilt. You need to install it (gem install tilt).

Plugins

Cuba provides a way to extend its functionality with plugins.

How to create plugins

Authoring your own plugins is pretty straightforward.

module MyOwnHelper
  def markdown(str)
    BlueCloth.new(str).to_html
  end
end

Cuba.plugin MyOwnHelper

That's the simplest kind of plugin you'll write. In fact, that's exactly how the markdown helper is written in Cuba::TextHelpers.

A more complicated plugin can make use of Cuba.settings to provide default values. In the following example, note that if the module has a setup method, it will be called as soon as it is included:

module Render
  def self.setup(app)
    app.settings[:template_engine] = "erb"
  end

  def partial(template, locals = {})
    render("#{template}.#{settings[:template_engine]}", locals)
  end
end

Cuba.plugin Render

This sample plugin actually resembles how Cuba::Render works.

Finally, if a module called ClassMethods is present, Cuba will be extended with it.

module GetSetter
  module ClassMethods
    def set(key, value)
      settings[key] = value
    end

    def get(key)
      settings[key]
    end
  end
end

Cuba.plugin GetSetter

Cuba.set(:foo, "bar")

assert_equal "bar", Cuba.get(:foo)
assert_equal "bar", Cuba.settings[:foo]

Contributing

A good first step is to meet us on IRC and discuss ideas. If that's not possible, you can create an issue explaining the proposed change and a use case. We pay a lot of attention to use cases, because our goal is to keep the code base simple. In many cases, the result of a conversation will be the creation of another tool, instead of the modification of Cuba itself.

If you want to test Cuba, you may want to use a gemset to isolate the requirements. We recommend the use of tools like dep and gs, but you can use similar tools like gst or bs.

The required gems for testing and development are listed in the .gems file. If you are using dep, you can create a gemset and run dep install.

cuba's People

Contributors

0xflotus avatar agis avatar amclain avatar asterite avatar bemurphy avatar cordoval avatar cyx avatar doridoridoriand avatar eddiezane avatar fabianrbz avatar foca avatar frodsan avatar grempe avatar inkel avatar juanmcuello avatar kitop avatar martinpoljak avatar omab avatar rangeroob avatar rkh avatar samnang avatar slowernet avatar soveran avatar topalovic avatar vangberg avatar womblemuppet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cuba's Issues

Improve performance by caching regexes?

Hi @soveran! ๐Ÿ˜„

I noticed that to consume segments a regex is created each and every time the segment tries to be matched:

matchdata = env[Rack::PATH_INFO].match(/\A\/(#{pattern})(\/|\z)/)

So I made this benchmark:

require_relative "./lib/cuba"

Cuba.define do
  on "users" do
    on ":id" do |id|
      on root do
        res.write "User #{id}"
      end

      on "projects" do
        on ":project_id" do |project_id|
          res.write "User #{id}, Project #{project_id}"
        end
      end
    end
  end
end

env1 = { "PATH_INFO" => "/users/1", "SCRIPT_NAME" => "/" }
env2 = { "PATH_INFO" => "/users/1/projects/2", "SCRIPT_NAME" => "/" }

time = Time.now
30_000.times do
  Cuba.call(env1)
  Cuba.call(env2)
end
puts Time.now - time

On my machine it takes about 4 seconds to complete.

Now I cache the regexes with this diff:

diff --git a/lib/cuba.rb b/lib/cuba.rb
index b12e9f3..b4090fe 100644
--- a/lib/cuba.rb
+++ b/lib/cuba.rb
@@ -5,6 +5,7 @@ class Cuba
   EMPTY   = "".freeze
   SEGMENT = "([^\\/]+)".freeze
   DEFAULT = "text/html; charset=utf-8".freeze
+  REGEXES = Hash.new { |h, pattern| h[pattern] = /\A\/(#{pattern})(\/|\z)/ }
 
   class Response
     LOCATION = "Location".freeze
@@ -211,7 +212,7 @@ class Cuba
   private :try
 
   def consume(pattern)
-    matchdata = env[Rack::PATH_INFO].match(/\A\/(#{pattern})(\/|\z)/)
+    matchdata = env[Rack::PATH_INFO].match(REGEXES[pattern])
 
     return false unless matchdata

I run the snippet above and it now takes 2 seconds.

Twice as fast!

Do you think this is a good change? I think it's harmless: the routes of an app are fixed (not dynamic) so that REGEXES hash will reach a reasonable maximum size. Plus it can help with routes like ":id" where they might be used in several places.

What do you think?

Use of partials

I would like to extract a partial with some html from my layout.mote.
I was unable to find a easier solution than this:

layout.mote

...
{{ this.partial('mypartial') }}
...

then inside views/mypartial.mote i have some html content.

<p>This is the content of my partial.</p>

Is this the right way to do it?

BTW I like this little framework A LOT.
๐Ÿ‘ Thx for the nice job ๐Ÿ‘

File sending

Would be nice if we had an API for sending files (using Rack::File?) which would set proper headers and stuff.

Serving static files using Rack::Static not working

I'm trying to serve static files from Cuba using Rack::Static @ ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]:

Cuba.use Rack::Static, :urls => %w("/css" "/js"), :root => "public"

But I keep on getting 404s. I tried all combinations - removing :root, moving /css from public/ to /, using :urls => { "/css/style.css" => ... using string keys instead of symbols in Rack::Static configuration ... - but nothing seams to work.

Thanks !

Add travis.ci support

I haven't ever done this for a project, and don't know if there is some internal CI for cuba, but it would be useful to have, even if just for testing pull requests. Is anyone working on this? If not, I'll take a look tomorrow.

Create a generator

Create a simple generator with basics configurations, default ones commented.

cuba new <projectName>

Creates:

  • A config.ru
  • The main cuba file with
    • 1 method and its route to show the simple text renderer.
    • 1 method, its view and route to show the view renderer.

That's it.

It keeps it small and speed up the dev process when using cuba for most use cases.

Proposal: remove `header` API

header API was introduced in bf5f628, but with no examples or instructions on how to use it and also no tests of it.

Actually from the name header, we assume it's used to ensure the request has some header with it, but http header in the env hash should be start with HTTP_ prefix. So the correct implementation might be

  ENV_HTTP_HEADER_PREFIX = "HTTP_".freeze
  def header(key)
    lambda { env["#{ENV_HTTP_HEADER_PREFIX}_#{key}".upcase.tr("-","_")] }
  end

Or the user has to call header('HTTP_KEYNAME') if he/she wants to make sure the KEYNAME exist in the header.

So, as I mentioned early, since there are no examples, instructions and tests on how to use it. Should cuba just remove it?

Or if you are concerned about breaking the backward compatibility, I will be happy to contribute some docs and tests on it.

rack 3.0 support

will cuba support rack 3.0?

here some of the changes that effect cuba.

  • Extract rackup command, Rack::Server, Rack::Handler and related code into a separate gem.
  • Moved Rack::Session into separate gem.
  • Response header keys can no longer include uppercase characters.
  • Rack::Headers added to support lower-case header keys.

i've included links to rack 3.0's changelog and upgrade guide as a connivence.

change log
https://github.com/rack/rack/blob/3.0.0/CHANGELOG.md

upgrade guide
https://github.com/rack/rack/blob/3.0.0/UPGRADE-GUIDE.md

When running sample test, I get: cannot load such file -- cutest (LoadError)

/Library/Ruby/Site/2.0.0/rubygems/core_ext/kernel_require.rb:55:in `require': cannot load such file -- cutest (LoadError)
        from /Library/Ruby/Site/2.0.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /Library/Ruby/Gems/2.0.0/gems/cuba-3.1.1/lib/cuba/test.rb:2:in `<top (required)>'
        from /Library/Ruby/Site/2.0.0/rubygems/core_ext/kernel_require.rb:135:in `require'
        from /Library/Ruby/Site/2.0.0/rubygems/core_ext/kernel_require.rb:135:in `rescue in require'
        from /Library/Ruby/Site/2.0.0/rubygems/core_ext/kernel_require.rb:144:in `require'

Confusion over params capture

I was asking in #cuba.rb on this. The channel consensus seems to be that on param() matching should match blank (non-nil) param values. That is how the code behaves now.

However, the README, example, and comments seem to imply it's for checking the presence of a real, non-blank value. I'm confused which it is.

If the behavior is wrong, I can submit a pull request with tests. Thanks.

undefined method `DelegateClass' for Rack::Session::Cookie:Class (NoMethodError)

/Ruby27/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/session/cookie.rb:155:in <class:Cookie>': undefined method DelegateClass' for Rack::Session::Cookie:Class (NoMethodError)
hele_world.rb

require "cuba"
require "cuba/safe"

Cuba.use Rack::Session::Cookie, :secret => "__a_very_long_string__"

Cuba.plugin Cuba::Safe

Cuba.define do
  on get do
    on "hello" do
      res.write "Hello world!"
    end

    on root do
      res.redirect "/hello"
    end
  end
end

Not all HTTP methods are supported

I noticed that matching for OPTIONS requests is not supported. I checked the source for Cuba and found out that matching for the existing methods is simple sugar on top of Rack (https://github.com/soveran/cuba/blob/master/lib/cuba.rb#L328-L331). I tried adding OPTIONS and the other missing HTTP methods in the same manner as the other methods have been implemented and it worked well.

I commited the new methods in my fork. There were no tests related to HTTP methods, so I didn't write any. The fork is ready for merge.

I think the added methods are a worthwhile patch. IMO, they are not a new feature but an extension to an existing one. The addition doesn't interfere with Cuba's minimalistic approach.

Reference in Readme to Cuba::TextHelpers

I quote:
That's the simplest kind of plugin you'll write. In fact, that's exactly how the markdown helper is written in Cuba::TextHelpers.

Where is Cuba::TextHelpers? Is it some cuba-contrib project I don't know about? Thanks

Missing rack session in v4

Hi @soveran,

With the last version, I have

/usr/src/app # bundle exec puma
Puma starting in single mode...
* Puma version: 6.1.0 (ruby 3.2.1-p31) ("The Way Up")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 7
! Unable to load application: LoadError: cannot load such file -- rack/session
bundler: failed to load command: puma (/usr/local/bundle/bin/puma)
<internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require': cannot load such file -- rack/session (LoadError)
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from /usr/local/bundle/gems/cuba-4.0.0/lib/cuba.rb:3:in `<top (required)>'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from <internal:/usr/local/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:37:in `require'
	from /usr/src/app/app.rb:3:in `<top (required)>'
	from config.ru:3:in `require_relative'
	from config.ru:3:in `block in <main>'
	from /usr/local/bundle/gems/rack-3.0.4.1/lib/rack/builder.rb:103:in `eval'
	from /usr/local/bundle/gems/rack-3.0.4.1/lib/rack/builder.rb:103:in `new_from_string'
	from /usr/local/bundle/gems/rack-3.0.4.1/lib/rack/builder.rb:94:in `load_file'
	from /usr/local/bundle/gems/rack-3.0.4.1/lib/rack/builder.rb:64:in `parse_file'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/configuration.rb:365:in `load_rackup'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/configuration.rb:287:in `app'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/runner.rb:158:in `load_and_bind'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/single.rb:44:in `run'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/launcher.rb:189:in `run'
	from /usr/local/bundle/gems/puma-6.1.0/lib/puma/cli.rb:75:in `run'
	from /usr/local/bundle/gems/puma-6.1.0/bin/puma:10:in `<top (required)>'
	from /usr/local/bundle/bin/puma:25:in `load'
	from /usr/local/bundle/bin/puma:25:in `<top (required)>'
	from /usr/local/lib/ruby/3.2.0/bundler/cli/exec.rb:58:in `load'
	from /usr/local/lib/ruby/3.2.0/bundler/cli/exec.rb:58:in `kernel_load'
	from /usr/local/lib/ruby/3.2.0/bundler/cli/exec.rb:23:in `run'
	from /usr/local/lib/ruby/3.2.0/bundler/cli.rb:491:in `exec'
	from /usr/local/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
	from /usr/local/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
	from /usr/local/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
	from /usr/local/lib/ruby/3.2.0/bundler/cli.rb:34:in `dispatch'
	from /usr/local/lib/ruby/3.2.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
	from /usr/local/lib/ruby/3.2.0/bundler/cli.rb:28:in `start'
	from /usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/libexec/bundle:45:in `block in <top (required)>'
	from /usr/local/lib/ruby/3.2.0/bundler/friendly_errors.rb:117:in `with_friendly_errors'
	from /usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/libexec/bundle:33:in `<top (required)>'
	from /usr/local/bin/bundle:25:in `load'
	from /usr/local/bin/bundle:25:in `<main>'

seems that rack-session is missing.

Regards,

Related to the-benchmarker/web-frameworks#6148

Allow Param defaults to be an empty string?

Hi,

Would make sense to allow empty strings as a default param value? While it makes sense to prevent empty strings from being captured from a request, the current implementation discards both empty params and empty defaults as they pass through the control flow. See: https://github.com/soveran/cuba/blob/master/lib/cuba.rb#L264

Would you be open to a pull request addressing this or would you guys be interested in tackling this small change?

Background Scenario/Usecase

I have a PORO JSON presenter/serializer wrapping an instance of data object, which together return validation errors. I'd like Cuba to pass the empty strings to my presenter/object, and could do so if I could set empty strings as my default. Currently, I'd have to have a second on true block that I'd rather not have cluttering up my code.

[Suggestion] Adds support for beerpay.io

Hi @soveran , first congrats for all your projects, I have been used many of them for a lot of projects, cuba, ost, micromachine, etc...
I want to invite to you to add some projects to beerpay.io, so I can support them , and retribute at least some of everything you do for us
about Beerpay, it is a free plataform for support open source project and help them to grow up, I'm inviting to all people, who has projects that I'm using, for support them

thanks for all!

How about defining secure headers in lower case to match the HTTP/2 specification?

Hi!

Currently, the HTTP/2 protocol specification defines secure headers, which are security-related headers, in uppercase, which is not consistent with the specification since HTTP/2 normally treats headers in lowercase.
Also, since secure headers are currently defined in uppercase, some Ruby application servers, such as Rack, treat this as a lint error or validation error, which prevents the web application from starting properly.
I would like to propose that secure headers be redefined in lowercase to match the HTTP/2 specification. I believe this will improve consistency in the context of the HTTP/2 protocol and avoid compatibility and application server launch issues.

Currently, the following workaround is available, but I believe that redefining it in lowercase would be a more desirable form.

res.headers.keys.each do |key|
  res.headers[key.downcase] = res.headers.delete(key)
end

Specific changes:
I would like to make a correction to redefine the following in lower case.
https://github.com/doridoridoriand/cuba/blob/change-secure-headers-to-lowercase/lib/cuba/safe/secure_headers.rb#L29-L36

References:
https://github.com/rack/rack/blob/main/lib/rack/lint.rb#L655-L656
https://datatracker.ietf.org/doc/html/rfc9113#name-http-fields

Regards,

Possibility to disable csrf in test environment

Problem running tests is that none of my queries are all of sudden not authorized b/c of missing csrf token in headers. Is there a way to simply disable it ? Or actually set a csrf token on the header for every request made in test mode ?

Custom res.staus stops rendering on chrome.

On chrome inputting a custom res.staus stops page from rendering html/css/js properly and
just prints the html in plain text though it shows the right status.

here is some of my code that displays incorrectly on chrome (works in firefox and edge):

class Login < Cuba; end
  Login.define do
    on get do
      on root do
        res.status = 401
        res.write view('/login')
      end
    end

settings can not use all objects when having a subclass and . . .

Cuba.settings[ :state ] = Proc.new { "ok" }
class A < Cuba; end

just does not work. some with singletons, lambdas, methods, etc

I solved the problem for myself with this plugin
https://github.com/mkristian/cuba-api/blob/master/lib/cuba_api/config.rb
which does implement a configuration with inheritance on class level:

Cuba.plugin CubaApi::Config
Cuba[ :state ] = Proc.new { "ok" }
class A < Cuba; end

and the value of can be changed later and A will see the change as well.

Cuba.plugin CubaApi::Config
Cuba[ :state ] = "ok"
Cuba.settings[ :state ] = "ok"
class A < Cuba; end
Cuba[ :state ] = "not-ok"
Cuba.settings[ :state ] = "not-ok"
p A[:state]
> "not-ok"
p A.settings[:state]
> "ok"

making a deep copy during class instantiation is also tricky since order of when the class gets defined does matter.

class A < Cuba; end
Cuba.settings[ :state ] = "ok"
class B < Cuba; end
p A.settings[:state]
> nil
p B.settings[:state]
> "ok"

although A and B are essentially the same class but were defined at different states of Cuba. having those A and B in their own files and require them makes it even harder to understand what is going on

require 'a'
Cuba.settings[ :state ] = "ok"
require 'b'
p A.settings[:state]
> nil
p B.settings[:state]
> "ok"

again using the cuby_api/config plugin makes the code more intuitive.

require 'a'
Cuba[ :state ] = "ok"
require 'b'
p A[:state]
> "ok"
p B[:state]
> "ok"

since intuition varies from person to person I would like to know other opinions.

BTW I would happy to prepare pull request to get something like cuby_api/config into Cuba and deprecate the current settings.

finally need to mention that I do like this little cuba framework a lot - I am about to switch my rails-api server to cuba/cuba_api and the new setup is so much easier to understand. I can understand the whole lot (including cuba) by reading half an hour code.

Manual Server start

I came to a situation where I needed to start a Cuba application by myself not using rackup or a config.ru file at all. Would be nice if we had an option to do so.

[OT] - Thank you

Hi! I'm one of the many users that love Cuba. I also have the pleasure to know you and enjoy all of your work and products but now I want to show my gratitude in a public manner.

This OT and useless issue it's just my dumb way to say thank you for the work you do, the support to the community and the lesscode initiative that makes a bunch of us happy.

Next time I'll buy you a coffee and give you a hug.

404 default Content-Type not set

The Content-Type isn't being set when a 404 occurs on our application, which means that plain text is rendered: http://cl.ly/image/1f401k340S29

Is this the intended default behavior?

I believe that the way it works is all 200's have a default Content-Type of text/html if not otherwise set, but other status codes have no Content-Type at all (see here)

Cuba::Render layouts not working

Hi, I'm having trouble using render from Cuba::Render - I get a LocalJumpError "no block given (yield)".
My setup is as follows:

# app.rb
require 'cuba'
require 'cuba/render'
require 'haml'
require 'tilt/haml'

Cuba.plugin Cuba::Render
Cuba.settings[:render][:template_engine] = 'haml'

Cuba.define do
  on root do
    render 'dashboard'
  end
end
-# views/layout.haml
!!!5
%html
  %head
    %title Title
  %body
    = yield
-# views/dashboard.haml
%h1 Dashboard

Accessing the root page gives the LocalJumpError as above.

How to mount Cuba app as Rack app?

I am trying to mount a Cuba app with Rack builder as follows:

builder = Rack::Builder.new do
  use Rack::AuthMiddleware, :users => Tokens
  use API

  run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['All responses are OK']] }
end

But apparently, a Cuba constructor takes no arguments, and I get a:

/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/cuba-3.1.0/lib/cuba.rb:102:in `initialize'
/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.4/lib/rack/builder.rb:82:in `new'
/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.4/lib/rack/builder.rb:82:in `block in use'
/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.4/lib/rack/builder.rb:130:in `[]'
/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.4/lib/rack/builder.rb:130:in `block in to_app'
/Users/pmu/.rbenv/versions/1.9.3-p194/lib/ruby/gems/1.9.1/gems/rack-1.4.4/lib/rack/builder.rb:130:in `each'

Any ideas how to get this working?

Optional query parameters

Hi, I have a question regarding optional query parameters. I have the following routes defined:

Cuba.define do
  on get do
    on 'movies' do
      on param("genre"), param("offset"), param("limit") do |genre, offset, limit|
        ...
      end
    end
  end
end

But this route only matches when exactly all query params are passed and I would like this route to match even if none are passed or just some. I mean, make these parameters are completely optional. Is there any way to achieve that in a simple way instead of using req.env["rack.request.query_hash"] directly?

Thanks

Having issues with cuba safe

The app is:

on "idea" do
  on get do
    token = csrf.token
    res.write({csrf: token}.to_json)
  end

  on post do
    p csrf.safe?
  end
end

In the tests:

test "ideas works" do
  csrf_token = JSON.parse(get("idea").body)["csrf"]
  post "idea", "csrf_token" => csrf_token
end

And i get false in the console
I inspected the session and every time it has different contents
So It does not get stored between requests. What am I doing wrong here?

List of "routes" declared in the app

It would be awesome to have a sort of rake routes to have the list of the defined URL that the Cuba app can match on.

If you give me some pointers into the code I can try to code it myself and submit a PR.

Thanks!

wrong number of arguments (given 0, expected 1..2) when upload file

  on get do
	on 'upload' do 
	    res.write '<form action="/multipart" enctype="multipart/form-data" method="post" ><input name="file" type="file" /><input type="submit" value="upload" /></form>'
		
	end
 end 
  on post do 

    on 'multipart' do 
	 
                 tmpfile =  param["file"][:tempfile]
		filename =  param["file"][:filename]
		target="public/upload/#{filename}"
		File.open(target,'wb') {
		|f| f.write(tmpfile.read) 
		}
		
	
    end
end

Strict-Transport-Security header in Cuba::Safe::SecureHeaders is dangerously strict!

The setting of the 'Strict-Transport-Security' security header is by default set to:

"Strict-Transport-Security" => "max-age=631138519; includeSubdomains; preload"

The use of this header is good, but for a default setting (encouraged to use in the docs with no explanation of its repercussions) it is far to strict and could cause a developers other websites to inadvertently become inaccessible for long periods of time to any browser that visits the SecureHeader Cuba app over HTTPS.

In detail:

max-age=631138519;

This is good to set, but it is set for a very long period for a developer that may only be in testing mode. This value represents 20.013 years. That means that any browser that successfully visits that site will never allow it to be visited over a non-secure HTTP connection for at least 20 years. The only way to reset is for the server to set it to '0' explicitly and then the browser user has to visit the site again. I would suggest a more reasonable value of 1 month, or 2628000 seconds.

includeSubdomains

This is an optional value and it has potentially far-reaching effects and should not be included in a default setup. By setting this, you are telling every browser that visits this site over an HTTPS connection successfully (even once) that ALL SUBDOMAINS of this domain shall also be REQUIRED to visit over an https connection. So for example, if there is a pre-existing www.example.com website which does not have a TLS certificate, and then the developer of this Cuba mini-app decides to setup this secure little mini-site on HTTPS. From now on every browser that visited the mini-site will refuse to load the www site over simple HTTP since it is not protected by TLS!! This (spec optional) setting should be removed from Cuba's defaults.

preload

preload is another optional part of the spec, and is dangerous to include in a default setup. By having this set you are requesting (or allowing to be requested) that this domain be permanently hard-coded into the Chrome browser (and others who use their service) that they should only be allowed to see the site over HTTPS. This setting, when present, might allow an attacker to add the site on the HSTS preload site (https://hstspreload.appspot.com) and force all Chrome users to only allow access to this domain, and all sub-domains, over HTTPS. This option should also be removed from the defaults.

All of this is well documented here:

https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security

Twitter Secure Headers project, also does not use the optional attributes:

https://github.com/twitter/secureheaders

You should also include instructions for how to override specific security settings as appropriate for a Cuba app in the docs. I am doing so in my Cuba app with something like this which seems to be working as expected:

require 'cuba'
require 'cuba/safe'
...
Cuba.plugin Cuba::Safe::SecureHeaders
Cuba.settings[:default_headers].merge!({"Strict-Transport-Security" => "max-age=2628000"})
...
Cuba.define do
  ...
end

Allow multi mount routes on define

For example I have this

on default do
    run V1::Settings
    run V1::Setups
end

Currently that does not work, I have a bunch of endpoint on each Cuba class and I'd like to mount them using only run method.

I'm not pretty sure but Grape mount the endpoints like my approach using the mount method.

Support for generators via CLI

Rails has a great CLI interface for starting off new applications and it also has support for generating controllers, models, views and schemas.

Is something like this already in plan for Cuba? I understand that it is planned for a micro-framework and it does a good job with that but something like a cuba new my-new-app would be a great addition.

List of new commands -

  • cuba new path/to/my-new-app
    This will generate a new application skeleton for a cuba based application. Features for customizing it can be provided.
  • cuba init
    This is something on the lines of how compass works. If there is a json file with the config information, a cuba app will be initialized in that folder.
  • cuba generate my-controller This will create a new controller and create a new test file based on the config. No views and models will be generated.

If a feature of this sort is welcome, I would love to work on it.

The dependencies in .gems.dev are not installed

I just followed the instruction from The Guide to Cuba, when I do "dep install", dep only install the dependency in .gems file and the gems defined in .gems.dev are not installed, do I need to install those gems manually or I can pass some argument like "dep install --develpment"?

Post Request with multiple parameters with the same name

Given I have a form on my website where I use a <select name="players" multiple>, my browser will send a POST request with the application/x-www-form-urlencoded request body "players=2&players=3". As you can see, it contains the key players twice.

If I now get the value of req.POST I get the value {"players"=>"3"}. The second occurrence of the key overwrote the first one. The only place where I can see all selections is in req.env["rack.request.form_vars"]. Here I will get the string "players=2&players=3".

So if I want to access the entire selection of the user, I will need to parse the form_vars by hand.

Is there any other solution? This also seems to be at the Rack level, right? So this is probably not even in the source code of Cuba. But maybe someone has a solution for it?

Rack 2.0 support?

Rails 5 and Rack 2 are out. Cuba is locked to Rack 1.6. Any plans for an upgrade?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.