I have a simple Sinatra app that I am playing with, and for some reason the cookies don't seem to work for certain routes, which I find quite bizarre.
require "sinatra"
set(:authenticate) do |*vars|
condition do
unless request.cookies.has_key?("TestCookie")
redirect to("/login"), 303
end
end
end
get "/login" do
return "No valid cookie"
end
get "/secret", :authenticate => [:auth_cookie] do
cookie = request.cookies["TestCookie"]
return "Secrets ahoy - #{cookie}"
end
get '/cookie/set' do
response.set_cookie("TestCookie", {
:expires => Time.now + 2400,
:value => "TestValue"
})
return "Cookie is set"
end
get '/cookie/get' do
cookie = request.cookies["TestCookie"]
return "Cookie with value #{cookie}"
end
If I go to cookies/set it correctly sets the cookie (can see it in firecookie), then if I go to cookies/get I get the correct cookie output. However if I go to /secret it always redirects to the /login. As I am still fairly new to Ruby syntax I thought it may be a problem with my condition within the authenticate extension, so I have tried removing that and just spitting out the cookie like the other one does. However still nothing, so I am at a loss as to why the cookie is there, I can see it in the browser... and /cookies/get works, but /secret doesn't...
Am I missing something here?
The problem is that the cookie is set with path /cookie. When you set a cookie your can specify a path, which is effectively a sub-part of the Website that you want the cookie to apply to. I guess Sinatra/Rack use the path of the current request by default which in /cookie/set would be /cookie.
You can make it work the way you expect by explicitly specifying the path:
response.set_cookie("TestCookie", {
:expires => Time.now + 2400,
:value => "TestValue",
:path => '/'
})
Or you could set the cookie at a route called say /cookie-set rather than /cookie/set
Related
I am building a Ruby app using Sinatra and the Twilio api.
Over a phone call to my assigned Twilio number, the user gets prompted to record an audio message. Once that message is recored the user gets redirected to the following route where if the user dials 1 (or anything else), they should get redirected to their feed of msgs, but if the user dials 2, then the user's message gets deleted and should get redirected to a route where they can record a new message.
Here is my route:
get '/playback/handle-recording/:recordingSID' do
if params['Digits'] = '2'
delete(params['recordingSID'])
deletedMsg = "Audio deleted."
getRecord(deletedMsg)
else
getFeed()
end
end
helper methods:
helpers do
def delete(recording)
recording = client().account.recordings.get(recording)
recording.delete
end
def getFeed()
redirect '/feed'
end
def getRecord(appendMsg)
Twilio::TwiML::Response.new do |response|
if appendMsg
response.Say appendMsg
end
response.Say "Record your message."
response.Record :maxLength => '5', :trim => "trim-silence", :playBeep => "true", :action => '/playback', :method => 'get'
end.text
end
end
My issue is that whether the user lands in the "if" or the "else" the first method, getRecord(deletedMsg) is the one that gets called, and not the getFeed().
How can I fix my route so that if the user lands in the else he does get redirected to his feed page and not to the getRecords method.
Are you sure they're actually making it into the else? Ruby won't just randomly not execute what's in there for no good reason.
One thing you may want to look at is you are assigning, not comparing the params value:
if params['Digits'] = '2'
You'll want to do:
if params['Digits'] == '2'
That could definitely lead to some strange behavior in an if statement, like, for instance always executing one path.
I've stumbled across a bit of problem when it comes to redirects behind a protected set of URLs (admin section) within a Sinatra app. It most likely a silly mistake but I haven't found anything online that helps.
This is for a password protected area as the helpers show, where the user can create new events. The first time a user tries to access the admin, they are prompted for a password, then subsequent pages are left. The problem I have is that when the app attempts to redirect after a successful new event is made, the user has to re-auth themselves ... which seems bit redundant.
This also applies for the deletion and editing process, the user always gets prompted when a redirect is attempted. I've tried passing 303 at the second parameter to for a different HTTP code, but to no avail
Anyway, here's the code, any questions/help would be appreciated
helpers do
def protected!
unless authorized?
response['WWW-Authenticate'] = %(Basic realm="Restricted Area")
throw(:halt, [401, "Not authorized\n"])
end
end
def authorized?
#auth ||= Rack::Auth::Basic::Request.new(request.env)
#auth.provided? && #auth.basic? && #auth.credentials && #auth.credentials == ['admin', 'admin']
end
end
...
get "/admin/events/:id" do
protected!
conf = Conference.where(:_id => params[:id]).first
not_found unless conf
haml :admin_event_edit, :layout => :admin_layout, :locals => { :event => conf }
end
post "/admin/events/new/" do
protected!
conf = Conference.new(params[:event])
if conf.save!
redirect "/admin/events/"
else
"Something went horribly wrong creating the new event, heres the form contents #{params.inspect}"
end
end
get "/admin/events/" do
protected!
haml :admin_events, :layout => :admin_layout, :locals => { :our_events => Conference.where(:made => true).order_by(:start_date.asc).limit(15), :other_events => Conference.where(:made => false).order_by(:start_date.asc).limit(15)}
end
Is this only happening in Safari?
I've used the code above and it only re-auths in Safari, Chrome, and FireFox work as expected.
It seems that if you unless you check the "remember my username/password" Safari will send each subsequent request without the Authorization in the header (a great tool for watching headers etc is Charles). If you do check it then Apple sends the Auth in the header correctly and even if you quit out of Safari it will continue to remember to send the Auth on relaunch.
So it's Apple being silly not you :)
Summary
Within a Sinatra web app, how can I make a virtual request to the application and get the response body back as text? For example, these routes...
get('/foo'){ "foo" }
get('/bar'){ "#{spoof_request '/foo'} - bar" }
...should result in the response "foo - bar" when requesting "/bar" with the web browser.
Motivation
My application has a page representing an bug entry, with lots of details about that bug entry: what version was the bug experienced in, how important is it, what tags are associated with it, to whom is the bug assigned, etc.
The user may edit individual pieces of data on this page interactively. Using my AJAXFetch jQuery plugin, JavaScript uses AJAX to swap out a read-only section of the page (e.g. the name of the person that this bug is assigned to) with an HTML partial form for editing just that section. The user submits the form, and AJAX makes a new request for the static version of that field.
In order to be DRY, I want the Haml view that creates the page to use the exact same request that AJAX makes when creating the individual static pieces. For example:
#notifications.section
%h2 Email me if someone...
.section-body= spoof_request "/partial/notifications/#{#bug.id}"
Not-Quite-Working Code
The following helper defining spoof_request worked under Sinatra 1.1.2:
PATH_VARS = %w[ REQUEST_PATH PATH_INFO REQUEST_URI ]
def spoof_request( uri, headers=nil )
new_env = env.dup
PATH_VARS.each{ |k| new_env[k] = uri.to_s }
new_env.merge!(headers) if headers
call( new_env ).last.join
end
Under Sinatra 1.2.3, however, this no longer works. Despite setting each of the PATH_VARS to the desired URI, the call( new_env ) still causes Sinatra to process the route for the current request, not for the specified path. (This results in infinite recursion until the stack level finally bottoms out.)
This question differs from Calling Sinatra from within Sinatra because the accepted answer to that (old) question does not maintain the session of the user.
The code I was using is more complex than the answer in the Sinatra README, but relied on the same mechanism. Neither my code nor the answer from the README worked under 1.2.3 due to a bug in that version. Both now work under 1.2.6.
Here's a test case of a simple helper that works:
require 'sinatra'
helpers do
def local_get(url)
call(env.merge("PATH_INFO" => url)).last.join
end
end
get("/foo"){ "foo - #{local_get '/bar'}" }
get("/bar"){ "bar" }
In action:
phrogz$ curl http://localhost:4567/foo
foo - bar
The following appears to work as needed under Sinatra 1.2.3:
ENV_COPY = %w[ REQUEST_METHOD HTTP_COOKIE rack.request.cookie_string
rack.session rack.session.options rack.input]
# Returns the response body after simulating a request to a particular URL
# Maintains the session of the current user.
# Pass custom headers if you want to set or change them, e.g.
#
# # Spoof a GET request, even if we happen to be inside a POST
# html = spoof_request "/partial/assignedto/#{#bug.id}", 'REQUEST_METHOD'=>'GET'
def spoof_request( uri, headers=nil )
new_env = env.slice(*ENV_COPY).merge({
"PATH_INFO" => uri.to_s,
"HTTP_REFERER" => env["REQUEST_URI"]
})
new_env.merge!(headers) if headers
call( new_env ).last.join
end
where Hash#slice is defined as:
class Hash
def slice(*keys)
{}.tap{ |h| keys.each{ |k| h[k] = self[k] } }
end
end
It feels like I'm missing what you are trying to do but why not just call the defined method?
get('/foo'){ "foo" }
get('/bar'){ "#{self.send("GET /foo")} - bar" }
That is one funky method name by the way. Don't ask me why it's even allowed.
PS. This only works pre version 1.2.3. DS.
I am trying to use OmniAuth to handle the OAuth flow for a small-ish Sinatra app. I can get 37signals Oauth to work perfectly, however I'm trying to create a strategy for Freshbooks Oauth as well.
Unfortunately Freshbooks require OAuth requests to go to a user specific subdomain. I'm acquiring the subdomain as an input and I then need to persistently use the customer specific site URL for all requests.
Here's what I've tried up to now. The problem is that the new site value doesn't persist past the first request.
There's to to be a simple way to achieve this but I'm stumped.
#Here's the setup -
def initialize(app, consumer_key, consumer_secret, subdomain='api')
super(app, :freshbooks, consumer_key, consumer_secret,
:site => "https://"+subdomain+".freshbooks.com",
:signature_method => 'PLAINTEXT',
:request_token_path => "/oauth/oauth_request.php",
:access_token_path => "/oauth/oauth_access.php",
:authorize_path => "/oauth/oauth_authorize.php"
)
end
def request_phase
#Here's the overwrite -
consumer.options[:site] = "https://"+request.env["rack.request.form_hash"]["subdomain"]+".freshbooks.com"
request_token = consumer.get_request_token(:oauth_callback => callback_url)
(session[:oauth]||={})[name.to_sym] = {:callback_confirmed => request_token.callback_confirmed?,
:request_token => request_token.token,
:request_secret => request_token.secret}
r = Rack::Response.new
r.redirect request_token.authorize_url
r.finish
end
Ok, here's a summary of what I did for anyone who comes across this via Google.
I didn't solve the problem in the way I asked it, instead I pushed the subdomain into the session and then I overwrite it whenever the site value needs to be used.
Here's the code:
#Monkeypatching to inject user subdomain
def request_phase
#Subdomain is expected to be submitted as <input name="subdomain">
session[:subdomain] = request.env["rack.request.form_hash"]["subdomain"]
consumer.options[:site] = "https://"+session[:subdomain]+".freshbooks.com"
super
end
#Monkeypatching to inject subdomain again
def callback_phase
consumer.options[:site] = "https://"+session[:subdomain]+".freshbooks.com"
super
end
Note that you still have to set something as the site when it's initialised, otherwise you will get errors due to OAuth not using SSL to make the requests.
If you want to see the actual code I'm using it's at: https://github.com/joeharris76/omniauth I'll push the fork up to the main project once I've battle tested this solution a bit more.
I'm working in Ruby, but my question is valid for other languages as well.
I have a Mechanize-driven application. The server I'm talking to sets a cookie using JavaScript (rather than standard set-cookie), so Mechanize doesn't catch the cookie. I need to pass that cookie back on the next GET request.
The good news is that I already know the value of the cookie, but I don't know how to tell Mechanize to include it in my next GET request.
I figured it out by extrapolation (and reading sources):
agent = Mechanize.new
...
cookie = Mechanize::Cookie.new(key, value)
cookie.domain = ".oddity.com"
cookie.path = "/"
agent.cookie_jar.add(cookie)
...
page = agent.get("https://www.oddity.com/etc")
Seems to do the job just fine.
update
As #Benjamin Manns points out, Mechanize now wants a URL in the add method. Here's the amended recipe, making the assumption that you've done a GET using the agent, and that the last page visited is the domain for the cookie (saves a URI.parse()):
agent = Mechanize.new
...
cookie = Mechanize::Cookie.new(key, value)
cookie.domain = ".oddity.com"
cookie.path = "/"
agent.cookie_jar.add(agent.history.last.uri, cookie)
These answers are old, so to bring this up to date, these days it looks more like this:
cookie = Mechanize::Cookie.new :domain => '.mydomain.com', :name => name, :value => value, :path => '/', :expires => (Date.today + 1).to_s
agent.cookie_jar << cookie
I wanted to add my experience for specifically passing cookies from Selenium to Mechanize:
Get the cookies from your selenium driver
sel_driver = Selenium::WebDriver.for :firefox
sel_driver.navigate.to('https://sample.com/javascript_login')
#login
sel_cookies = sel_driver.manage.all_cookies
Value for :expires from Selenium cookie is a DateTime object or blank.
However, value for :expires Mechanize cookie (a) must be a string and (b) cannot be blank
sel_cookies.each do |c|
if c[:expires].blank?
c[:expires] = (DateTime.now + 10.years).to_s #arbitrary date in the future
else
c[:expires] = c[:expires].to_s
end
end
Now instantiate as Mechanize cookies and place them in the cookie jar
mech_agent = Mechanize.new
sel_cookies.each { |c| agent.cookie_jar << Mechanize::Cookie.new(c) }
mech_agent.get 'https://sample.com/html_pages'
Also you can try this
Mechanize::Cookie.parse(url, "SessionCookie=#{sessid}",
Logger.new(STDOUT)) { |c| agent.cookie_jar.add(url, c) }
source: http://twitter.com/#!/calebcrane/status/51683884341002240
response.to_hash.fetch("set-cookie").each do |c|
agent.cookie_jar.parse c
end
response here is a native Ruby stdlib thing, like Net::HTTPOK.