Ruby/Sinatra send_file not working - ruby

I'm using send_file on a Sinatra app:
get '/update/dl/:upd' do
filename ="/uploads/#{params[:upd]}"
send_file(filename, :filename => "t.cer", :type => "application/octet-stream")
end
The folder /uploads/ it's not public, it's on the app dir. When I try to go to localhost:4567/update/dl/some_file in Chrome it returns me a 404, like with Firefox, when seeing the headers, it's a 404. But if I try with Safari it downloads the file. So I guess somthing's wrong with my code (and Safari's, but let's left that to Apple :P). What could be wrong? Thanks!

I get it to work fine in chrome if I remove the initial slash in filename so it's "filename instead of "/filename. The 404 comes from a file not found error in send_file
# foo.rb
require 'sinatra'
get '/update/dl/:upd' do
filename ="uploads/#{params[:upd]}"
# just send the file if it's an accepted file
if filename =~ /^[a-zA-Z0-9]*.cer$/
send_file(filename, :filename => "t.cer", :type => "application/octet-stream")
end
end
However, there's really a big security hole in this, a user can download anything that the sinatra process has access too, I named my sinatra app foo.rb and this request downloads the sinatra script:
http://localhost:4567/update/dl/..%2Ffoo.rb

Related

How can I execute Ruby code with WEBrick instead of dumping the code to my browser?

I'm facing a problem when I run my program in a browser with the WEBrick server. It shows me my code as written in the 2loop.rb file.
When I run ruby -run -e -httpd. -p 5000 at the command prompt, and load http://localhost:5000/2loop.rb in the browser, it shows the code from 2loop.rb instead of running it.
How can I execute the 2loop.rb program instead?
TL;DR
You're doing this to yourself by serving your current working directory as the root of your web server. You aren't actually running the code in your file; you're just telling WEBrick to serve any file you name in the URI. http://localhost:5000/2loop.rb will serve "2loop.rb" as text/html in your posted example.
Using un.rb
The flag you're using isn't actually "run." Instead, the -r flag actually loads a module, which in this case is the un.rb module. Using un.rb to start WEBrick is done like this:
$ ruby -run -e httpd . -p 5000
and starts a web server in the document root. In this case, the dot means to use the current working directory as the root. This is not really what you want to start code you've placed inside a Ruby file.
Running WEBrick Programmatically
Using some snippets from the WEBrick documentation, you will see that you can create a file named "2loop.rb" containing the following:
#!/usr/bin/env ruby
require 'webrick'
root = File.path '/tmp/public_html'
server = WEBrick::HTTPServer.new :Port => 5000, :DocumentRoot => root
trap 'INT' do server.shutdown end
server.start
This will serve files out of the /tmp/public_html directory on port 5000, which you can reach at http://localhost:5000. You can then make the file executable and start the server with ./2loop.rb, or just run ruby 2loop.rb if you don't want to make your file executable for some reason.
If you don't want WEBrick just to serve files, you will have to add custom behavior to your web server inside the 2loop.rb script. This is a fairly low-level thing to do, but may suit your needs.
Sensible Alternatives
You should probably use a web framework like Ruby on Rails or Sinatra if you don't want to have write all the low-level behaviors yourself. Sinatra in particular is a very lightweight alternative. This example:
#!/usr/bin/env ruby
require 'sinatra'
set :port, 5000
get '/hello' do
"Hello, World!"
end
will create a URL at http://localhost:5000/hello with a custom action that returns "Hello, World!" as an in-browser response.
Well, I'd suggest you to use Common Gateway Interface (CGI). Let me provide you an example.
Firstly, create a file named server.rb:
require 'webrick'
server = WEBrick::HTTPServer.new(
:Port => 6789, # a server's port
:DocumentRoot => File.join(Dir.pwd, "/scripts") # a folder with scripts
)
server.start
Secondly, create a folder scripts and put the following file (the_best_program.cgi) into it. Note the .cgi extension. It matters. Look here for details on the first line of the script (shebang) if you are working under Windows.
#!/usr/bin/env ruby
require 'cgi'
print "Content-type: text/plain\n\n"
5.times { |i| puts "Hello world #{i}!"}
puts 'So many worlds there. :('
Finally,
Launch your server from command-line (ruby server.rb).
Start browser and go to localhost:6789/the_best_program.cgi (or 0.0.0.0:6789/the_best_program.cgi)
Enjoy!
Notes
You might need to change permissions to your scripts folder / script. On unix-like system do: chmod 755 scripts scripts/the_best_program.cgi.
You can launch not only ruby scripts this way.

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)

Getting Rack mounted path in Sinatra application

Suppose I have the following config.ru file
require './status.rb'
map "/status" do
run Sinatra::Application
end
and the status.rb is a simple
require 'sinatra'
get '/' do
'Some status here...'
end
I'd like to know where the Sinatra application is mounted inside status.rb (for example to provide proper paths to resources). Is there a way of retrieving that information from Rack?
To get where the app is mounted you can use request.script_name.
get '/' do
p request.script_name # will print "/status"
'Some status here...'
end
If you’re generating urls for resources, you might want to look at the url method instead. That will take into account things like proxies as well as where the app is mounted:
get '/' do
p url('foo') # will print "http://localhost:9292/status/foo"
'Some status here...'
end

passing arguaments to a system call in Rails, but not plain ruby program

Ubuntu 12.04
Sinatra 1.3.3
Why does passing an argument to a ruby system call (%x[] or ``) give me a 'not found' error in my sinatra app? The same code works fine in a normal ruby script running from the same directory.
I have a file test.rb like this
output = %x["ls"]
p output
When I run it with "ruby test.rb" I get the contents of the current directory in the console, as expected.
If I modify the program to give an argument to the system call like so:
output = %x["ls sub_dir/"]
p output
I get the contents of sub_dir, which sits in the current directory, as expected.
So far so good.
Now if I make a Sintra app with a post method:
require 'rubygems'
require 'bundler/setup'
require 'sinatra'
post "/" do
output = x["ls"]
return output
end
The response to a Post call to "/" returns the contents of the current directory, which includes 'sub_dir', as expected.
If I try to add the argument to the system call to the sinatra app like so:
require 'rubygems'
require 'bundler/setup'
require 'sinatra'
post "/" do
output = x["ls sub_dir/"]
return output
end
the response is nil and there is an error in the console:
sh: 1: ls sub_dir/: not found
Why does adding a parameter to a system call in my sinatra app cause it to crash, when the same code called from a plain ruby script, run from the same location works perfectly.
By the way, the 'ls' example shown here is not the command I really need to run, so please don't explain a different way to get this information. I have an executable file that takes a file name as a parameter that I need to run, which behaves exactly the same way.
Thanks in advance!
If you want to specify a path in relation to the application, you could use something like this:
post "/" do
path = File.join(File.dirname(__FILE__), "sub_dir")
%x[ls #{path}]
end
However, if you want to list the contents of a directory, why not do it in Ruby?
I rewrote the sinatra app in another file in the same directory.
Everything works as expected.
I did not find the reason and I deleted the original file so that I won't lose anymore time trying to figure it out.

Sinatra - Setting Cache-Control Headers via config.ru

I'm currently running an Octopress (based on Jekyll) site on Heroku's Cedar stack—the code lives here: https://github.com/elithrar/octopress
I want to selectively apply a Cache-Control header based on the file type:
.html files get a value of public, max-age=3600
.css|.js|.png|.ico (etc) get a value of public, max-age=604800 - alternatively, I'd like to apply this rule to anything served from the /stylesheets', '/javascripts', '/imgs' directories.
Have used both set :static_cache_control , [:public, :max_age => 3600] and just the vanilla cache_control :public, :max_age => 3600 statements with no luck.
I have managed to set public, max-age=3600 on the articles themselves (e.g. /2012/lazy-sundays/), but have not been able to get the headers to apply to the CSS/JS (e.g. /stylesheets/screen.css)
My config.ru currently looks like this (updated):
require 'bundler/setup'
require 'sinatra/base'
# The project root directory
$root = ::File.dirname(__FILE__)
class SinatraStaticServer < Sinatra::Base
get(/.+/) do
cache_control :public, :max_age => 7200
send_sinatra_file(request.path) {404}
end
not_found do
send_sinatra_file('404.html') {"Sorry, I cannot find #{request.path}"}
cache_control :no_cache, :max_age => 0
end
def send_sinatra_file(path, &missing_file_block)
file_path = File.join(File.dirname(__FILE__), 'public', path)
file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i
File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
end
end
use Rack::Deflater
run SinatraStaticServer
Here's how to set long expiry headers for static assets, and an arbitrary expiry header for you main content on Heroku:
gemfile:
gem 'rack-contrib'
config.ru:
require 'rack/contrib'
get '*.html' do |page|
# whatever code you need to serve up your main pages
# goes here... use Rack::File I guess.
page
end
# Set content headers for that content...
before do
expires 5001, :public, :must_revalidate
end
# Assets in /static/stylesheets (domain.com/stylesheets)
# are served by Rack StaticCache, with a default 2 year expiry.
use Rack::StaticCache, :urls => ["/stylesheets"], :root => Dir.pwd + '/static'
run Sinatra::Application
By default that will give you a 2 year expiry for content listed in the array of urls (static/stylesheets, static/images etc.).
You have to move from /public to /static because otherwise you are unnecessarily fighting with Heroku's nginx config (the right place to apply these sorts of settings really...).
I know you said you're trying to not use Rack Contrib but that makes no sense. There's no harm in using a tiny 90 line library to do this https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/static_cache.rb.
The "right" way would be to host static content on an environment where you can configure nginx, and the second best way is renaming your static file path so heroku ignores it, and use rack static to serve static files with the headers you want.
--
Also to be clear, simply renaming your public folder to something else will allow you to do this via routes, and the normal Sinatra expires function. But I'd use StaticCache because it's less verbose. (The real issue is Heroku doesn't let nginx talk to your app for requests to public/, I believe.)
I have very little familiarity with Sinatra, but I think something like this would do the trick:
class SinatraStaticServer < Sinatra::Base
before '*.html' do
response.headers['Cache-Control'] = 'public, max-age=3600'
end
before %r{\.(css)|(js)|(png)|(ico)} do
response.headers['Cache-Control'] = 'public, max-age=604800'
end
# ...
end
Update: I looked into it further when you said that the above was not successfully getting the headers added. I determined that the issue was that Sinatra was automatically serving the files out of public/ rather than going through the app, and thus the headers weren't being added. My solution was to move the static files from public/ to public/public/ and adjust send_sinatra_file accordingly:
class SinatraStaticServer < Sinatra::Base
# ...
def send_sinatra_file(path, &missing_file_block)
file_path = File.join(File.dirname(__FILE__), 'public/public', path)
file_path = File.join(file_path, 'index.html') unless file_path =~ /\.[a-z]+$/i
File.exist?(file_path) ? send_file(file_path) : missing_file_block.call
end
# ...
end
I confirmed that this works on my machine. Note that I used response.headers['Cache-Control'] as in the first part of my answer, not set :static_cache_control which you tried, but I think is meant to only be run once, in a configure do block.
Also note that with this current set-up, a 404 that matches the above, e.g. nonexistant.png will serve a 404 status with the Cache-Control header still there. I can see several ways around that, but I figure you do to, so I'm just pointing it out and figure you'll deal with it however you like.

Resources