Metaprogramming sinatra get - ruby

I have a list of words in a list, and I want to handle get requests to any of them (and respond the same way).
#words = ["foo","bar"....etc]
One of the ways I thought I could do this is to loop through the list and have a get directive generated for each word when sinatra is launched.
#words.each do |word|
get word do
# what to do
end
end
that doesn't work, but something in that fashion, maybe.
Another way of doing it might be responding to get %r{/(.+)} and then doing some processing inside there to see if it matches anything in the list and respond accordingly, but I'm interested nonetheless to see if there's a way I can do it as described above.

What you wrote does work, with a very minor change. For example:
require 'sinatra'
%w[foo bar].each do |word|
get "/#{word}" do
"You said #{word}!"
end
end
$ curl http://localhost:4567/bar
You said bar!
Instead of a catch-all regex route, you can craft a custom regexp dynamically to match only the words you want to match:
require 'sinatra'
words = %w[foo bar]
route_re = %r{^/(#{words.join '|'})$}i # case-insensitive
p route_re
#=> /^\/(foo|bar)$/i
get route_re do |name|
"You said #{name}!"
end
$ curl http://localhost:4567/FoO
You said FoO!
$ curl -I http://localhost:4567/jim
HTTP/1.1 404 Not Found
X-Cascade: pass
Content-Type: text/html;charset=utf-8
Content-Length: 413
Connection: keep-alive
Server: thin 1.2.7 codename No Hup

Depending on what you need, this might be enough:
get '/:word' do
# check if params[:word] is in your list
# and respond accordingly
if #words.include?(params[:word])
"yeeah"
else
pass
end
end
But keep in mind that this route matches everything.

Related

Altering params in Sinatra before blocks

I am trying to alter the request params in a before block in Sinatra.
It seems that when using this syntax, it works:
before do
#params['global-before'] = 'yes'
end
But when using this syntax, it does not work:
before '/:id/test' do
#params['route-before'] = 'yes'
end
Here is a full example:
# test.rb
require 'sinatra'
require 'sinatra/reloader'
set :bind, '0.0.0.0'
set :port, 3000
before do
#params['global-before'] = 'yes'
end
before '/:id/test' do
#params['route-before'] = 'yes'
end
get '/' do
params.to_json
end
get '/:id/test' do
params.to_json
end
Then running:
$ ruby test.rb
$ curl localhost:3000
{"global-before":"yes"}
$ curl localhost:3000/123/test
{"global-before":"yes","id":"123"}
I was expecting to see the params['route-before'] populated as well.
I have tried using request.params instead of #params but that did not work at all.
Can anyone shed some light on this?
Update:
Opened an issue in Sinatra's issue tracker
The route filter goes first, and it has route parameters: {"id"=>"123"}, so this happens:
original, #params = #params, #params.merge(params) if params.any?
where original ends up as {} and #params as {"id"=>"123"}. When the global filter runs, there are no route parameters, so original remains unassigned (nil) and #params is the {} that was originally there.
After the filter processes, in the ensure clause, there is this:
#params = original if original
So global filter skips it, because original is nil, because there were no route parameters. The route filter resets the #params to whatever it was before the filter ran, because original is preserved, because there were route parameters.
I can't say whether this is a bug or an intended behaviour, but it's a "how" at least, if not a "why". It may make sense asking the Sinatra team (and reporting back here with the verdict).
tl;dr: #params are reset to pre-filter state if there are parameters in the filter's path pattern.
Note: you can hack around it by making your own instance variable:
before '/:id/test' do
#route_before = 'yes'
end
get '/:id/test' do
"Did we run route before? #{#route_before}"
end

How do I get all requests in puma rack app?

I'm writing a dead-simple puma server,
and I keep getting 404 for every request.
How do I configure the '/' path without Sinatra?
I simply want to catch all requests.
#config.ru
module Moon
class HelloWorldApp
def call(env)
[200, {}, 'Hello World']
end
def each(env=nil)
env
end
end
end
run Rack::Cascade.new Moon::HelloWorldApp.new
I run it like this:
$ puma config.ru -p 9595
Use this curl:
$ curl http://0.0.0.0:9595/test
And get 404:
127.0.0.1 - - [07/Apr/2015:22:49:25 +0300] "GET /test HTTP/1.1" 404 - 0.0002
Rack::Cascade expects an explicit array of apps to be passed to the constructor (or at least something like an enumerable that responds to each and yields the apps). It then calls each on this array to get each app (it looks like you’ve hit the no method error and tried to work round it by adding an each method to your app).
Fix it by changing the run line to:
run Rack::Cascade.new [Moon::HelloWorldApp.new]
You will also need to change the body part of your returned array, it needs to be something that responds to each and yields Strings, the simplest way to fix that is to return an array:
[200, {}, ['Hello World']]

Ruby/Rails: how to handle incoming URLs with ruby 1.8 UTF-8 encodings (like \xc3\xa1)

We're cleaning up some errors on our site after migration from ruby 1.8.7 to 1.9.3, Rails 3.2.12. We have one encoding error left -- Bing is sending requests for URLs in the form
/search?q=author:\"Andr\xc3\xa1s%20Guttman\"
(This reads /search?q=author:"András Guttman", where the á is escaped).
In fairness to Bing, we were the ones that gave them those bogus URLs, but ruby 1.9.3 isn't happy with them any more.
Our server is currently returning a 500. Rails is returning the error "Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ASCII-8BIT"
I am unable to reproduce this error in a browser, or via curl or wget from OS X or Linux command line.
I want to send a 301 redirect back with a properly encoded URL.
I am guessing that I want to:
detect that the URL has old UTF-8 then if it is malformed, only
use String#encode to get from old to new UTF-8
use CGI.escape() to %-encode the URL
301 redirect to the corrected URL
So I have read a lot and am not sure how (or if) I can detect this bogus URL. I need to detect because otherwise I would have to 301 everything!
When I try in irb I get these results:
1.9.3p392 :015 > foo = "/search?q=author:\"Andr\xc3\xa1s%20Guttman\""
=> "/search?q=author:\"András%20Guttman\""
1.9.3p392 :016 > "/search?q=author:\"Andr\xc3\xa1s%20Guttman\"".encoding
=> #<Encoding:UTF-8>
1.9.3p392 :017 > foo.encoding
=> #<Encoding:UTF-8>
I have read this SO post but I am not sure if I have to go this far or even if this applies.
[Update: since posting, we have added a call to the code in the SO post linked above prior to all requests.]
So the question is: how can I detect the old-style encoding so that I can do the other steps.
First, let's look at the string manipulation side of things. It looks to like using the URI module and unescaping then re-escaping will just work:
2.0.0p0 :007 > foo = "/search?q=author:\"Andr\xc3\xa1s%20Guttman\""
=> "/search?q=author:\"András%20Guttman\""
2.0.0p0 :008 > URI.unescape foo
=> "/search?q=author:\"András Guttman\""
2.0.0p0 :009 > URI.escape URI.unescape foo
=> "/search?q=author:%22Andr%C3%A1s%20Guttman%22"
So the next question is where to do that? I'd say the problem with trying to detect string with the \x escape character is that you can't GUARANTEE those strings were not supposed to be slash-x versus escaped (although, in practice, maybe that is an okay assumption).
You might consider just adding a small rack middleware that does this. See this Railscast for more on rack. Assuming you only get these in the parameters (i.e., after the ? in the URL), then your middleware would look something like (untested, just for illustration; place in your /lib folder as reescape_parameters.rb):
require 'uri' # possibly not needed?
class ReescapeParameters
def initialize(app)
#app = app
end
def call(env)
env['QUERY_STRING'] = URI.escape URI.unescape env['QUERY_STRING']
status, headers, body = #app.call(env)
[status, headers, body]
end
end
Then you use the middleware by adding a line to your application config or an initializer. For example, in /config/application.rb (or, alternatively, in an initializer):
config.middleware.use "ReescapeParameters"
Note that you will probably need to catch theme parameters before any parameter handling by Rails. I'm not sure where in the Rack stack you'll need to put it, but you will more likely need:
config.middleware.insert_before ActionDispatch::ParamsParser, ReescapeParameters
Which would put it in the stack before ActionDispatch::ParamsParser. You'll need to figure out the correct module to put it after. This is just a guess. (FYI: There is an insert_after as well.)
UPDATE (REVISED)
If you MUST detect these and then send a 301, you could try:
def call(env)
if env['QUERY_STRING'].encoding.name == 'ASCII-8BIT' # could be 'ASCII_8BIT' ?
location = URI.escape URI.unescape env['QUERY_STRING']
[301, {'Content-Type' => 'text','Location' => location}, '']
else
status, headers, body = #app.call(env)
[status, headers, body]
end
end
This is a trial -- it might match everything. But hopefully, "regular" strings are being encoded as something else (and hence you only get the error for the ASCII-8BIT encoding).
Per one of the comments, you could also convert instead of unescape and escape:
location = env['QUERY_STRING'].encode('UTF-8')
but you might still need to URI escape the resulting string anyway (not sure, depends on your circumstances).
Please use CGI::unescapeHTML(string)

Sinatra/Rack session.fetch producing unexpected results

I was writing a quick helper in Sinatra for redirect_to_next, where I redirect to the path provided by session[:next] if it exists, or a default.
In Sinatra, session really provided by Rack, and by spec, it is said to provide a hash like interface for fetch. I wrote the following error helper to explain my problem.
error 401 do
session[:next] = request.path
puts "get #{session[:next]}"
puts "fetch #{session.fetch(:next, '/')}"
redirect "/login"
end
When I attempt to access /settings when not logged in, I halt 401 which runs the above code. This is what it prints to my terminal:
get /settings
fetch /
:next exists as a key, so why is it giving me the default as if it does not?
Update
This minimal example shows the same behavior.
require 'sinatra'
set :sessions, true
get '/' do
session[:testing] = "hello"
puts "get #{session[:testing]}"
puts "fetch #{session.fetch(:testing, 'goodbye')}"
end
Logs
[2012-04-29 14:11:51] INFO WEBrick::HTTPServer#start: pid=1954 port=9292
get hello
fetch goodbye
10.0.2.2 - - [29/Apr/2012 14:11:54] "GET / HTTP/1.1" 200 - 0.0485
Software
ruby (1.9.3p194)
rack (1.4.1)
sinatra (1.3.2)
The session hash isn’t a normal Ruby Hash, it’s a Rack::Session::Abstract::SessionHash. SessionHash actually inherits from Hash, but it overrides the []= and [] methods, calling to_s on any keys before storing and retrieving them.
Extending your update example:
require 'sinatra'
set :sessions, true
get '/' do
session[:testing] = "hello"
puts "get #{session[:testing]}"
puts "fetch #{session.fetch(:testing, 'goodbye')}"
puts "fetch with string #{session.fetch(:testing.to_s, 'goodbye')}"
end
gives this output:
get hello
fetch goodbye
fetch with string hello
When you use Hash#fetch, passing a symbol, the method gets dispatched directly to the parent hash, without being converted to a string, and so the matching key isn’t found.
So, always use Strings as keys in your sessions and everything should work.

Get CGI params (query_string, path_info, etc) in Ruby

I'm trying to get the query_string from a Ruby file. For example;
http://localhost/rubyfile.rb?hello=world
I would like to be able to ask what's hello and for it to print "world", but for the life of me I cannot find the correct syntax/way to do it anywhere. Even the Ruby documentation seems dazed.
#!/program files (x86)/ruby/bin/ruby
require 'cgi'
cgi_request = CGI::new("html4")
This simply starts a new CGI spawn when the file is run, but how do I find the query_string?
puts cgi.params[query_string]
Doesn't seem to work-- I assume there is something i'm completly missing and im stupid but...
It should be simple, shouldn't it?
Thanks
The following should work:
require "cgi"
cgi_request = CGI::new("html4")
puts "Content-Type: text/html; charset=UTF-8"
puts
puts cgi_request['hello']
puts cgi_request.query_string
puts cgi_request.params['hello']

Resources