Serving multiple TLDs from one Rails server - ruby-on-rails

I currently have a single machine, running a stack that includes nginx/Passenger/Rails 3.x. This machine has several IP addresses, one for each TLD we're serving, primarily for SSL certificate purposes.
I'm having an issue with longer TLDs (.co.uk, .com.au, and their ilk.) My first attempt was the following filter in my ApplicationController:
if request.host =~ /co\.uk$|com\.au$/
ActionDispatch::Http::URL.tld_length = 2
else
ActionDispatch::Http::URL.tld_length = 1
end
Unfortunately, this runs after the routes have already been parsed, which means my subdomain handling in the config/routes.rb is already finished (and wrong). But, rather dangerously, I'm permanently affecting this passenger instance for the NEXT request. So it's breaking not only on this request for .com.au, but probably the next request for .com as well.
I have a feeling that what I should be doing is setting tld_length in my environment file, but I'd need to do it based on the IP address of the interface the user has connected to (since I have a different IP address for each TLD).
Is there an easy way to do this? If not, is there a better way to handle multiple TLDs on the same machine?

Looks like middleware is the way to go. Here's the solution that ended up working for me.
# config/application.rb
config.middleware.use TldHandler
And:
# app/lib/tld_handler.rb
class TldHandler
def initialize(app)
#app = app
end
def call(env)
TldHandler.enable_long_tlds(env['SERVER_NAME'])
status, headers, response = #app.call(env)
end
def self.enable_long_tlds(hostname)
if hostname =~ /co\.uk$|com\.au$/
ActionDispatch::Http::URL.tld_length = 2
else
ActionDispatch::Http::URL.tld_length = 1
end
end
end
You could continue to expand the regex as desired.

Related

Rails: Convert REST API to websocket client

I have a typical Rails REST Api written for a http consumers. However, it turns out they need web socket API because of the integration POS Machines.
The typical API looks like this;
class Api::Pos::V1::TransactionsController < ApplicationController
before_action :authenticate
def index
#transactions = #current_business.business_account.business_deposits.last(5)
render json: {
status: 200,
number: #transactions.count,
transactions: #transactions.as_json(only: [:created_at, :amount, :status, :client_card_number, :client_phone_number])
}
end
private
def request_params
params.permit(:account_number, :api_key)
end
def authenticate
render status: 401, json: {
status: 401,
error: "Authentication Failed."
} unless current_business
end
def current_business
account_number = request_params[:account_number].to_s
api_key = request_params[:api_key].to_s
if account_number and api_key
account = BusinessAccount.find_by(account_number: account_number)
if account && Business.find(account.business_id).business_api_key.token =~ /^(#{api_key})/
#current_business = account.business
else
false
end
end
end
end
How can i serve the same responses using web-sockets?
P.S: Never worked with sockets before
Thank you
ActionCable
I would second Dimitris's reference to ActionCable, as it's expected to become part of Rails 5 and should (hopefully) integrate with Rails quite well.
Since Dimitris suggested SSE, I would recommend against doing so.
SSE (Server Sent Events) use long polling and I would avoid this technology for many reasons which include the issue of SSE connection interruptions and extensibility (websockets allow you to add features that SSE won't support).
I am almost tempted to go into a rant about SSE implementation performance issues, but... even though websocket implementations should be more performant, many of them suffer from similar issues and the performance increase is often only in thanks to the websocket connection's longer lifetime...
Plezi
Plezi* is a real-time web application framework for Ruby. You can either use it on it's own (which is not relevant for you) or together with Rails.
With only minimal changes to your code, you should be able to use websockets to return results from your RESTful API. Plezi's Getting Started Guide has a section about unifying the backend's RESTful and Websocket API's. Implementing it in Rails should be similar.
Here's a bit of Demo code. You can put it in a file called plezi.rb and place it in your application's config/initializers folder...
Just make sure you're not using any specific Servers (thin, puma, etc'), allowing Plezi to override the server and use the Iodine server, and remember to add Plezi to your Gemfile.
class WebsocketDemo
# authenticate
def on_open
return close unless current_business
end
def on_message data
data = JSON.parse(data) rescue nil
return close unless data
case data['msg']
when /\Aget_transactions\z/i
# call the RESTful API method here, if it's accessible. OR:
transactions = #current_business.business_account.business_deposits.last(5)
write {
status: 200,
number: transactions.count,
# the next line has what I think is an design flaw, but I left it in
transactions: transactions.as_json(only: [:created_at, :amount, :status, :client_card_number, :client_phone_number])
# # Consider, instead, to avoid nesting JSON streams:
# transactions: transactions.select(:created_at, :amount, :status, :client_card_number, :client_phone_number)
}.to_json
end
end
# don't disclose inner methods to the router
protected
# better make the original method a class method, letting you reuse it.
def current_business
account_number = params[:account_number].to_s
api_key = params[:api_key].to_s
if account_number && api_key
account = BusinessAccount.find_by(account_number: account_number)
if account && Business.find(account.business_id).business_api_key.token =~ /^(#{api_key})/
return (#current_business = account.business)
end
false
end
end
end
Plezi.route '/(:api_key)/(:account_number)', WebsocketDemo
Now we have a route that looks something like: wss://my.server.com/app_key/account_number
This route can be used to send and receive data in JSON format.
To get the transaction list, the client side application can send:
JSON.stringify({msg: "get_transactions"})
This will result in data being send to the client's websocket.onmessage callback with the last five transactions.
Of course, this is just a short demo, but I think it's a reasonable proof of concept.
* I should point out that I'm biased, as I'm Plezi's author.
P.S.
I would consider moving the authentication into a websocket "authenticate" message, allowing the application key to be sent in a less conspicuous manner.
EDIT
These are answers to the questions in the comments.
Capistrano
I don't use Capistrano, so I'm not sure... but, I think it would work if you add the following line to your Capistrano tasks:
Iodine.protocol = false
This will prevent the server from auto-starting, so your Capistrano tasks flow without interruption.
For example, at the beginning of the config/deploy.rb you can add the line:
Iodine.protocol = false
# than the rest of the file, i.e.:
set :deploy_to, '/var/www/my_app_name'
#...
You should also edit your rakefile and add the same line at the beginning of the rakefile, so your rakefile includes the line:
Iodine.protocol = false
Let me know how this works. Like I said, I don't use Capistrano and I haven't tested it out.
Keeping Passenger using a second app
The Plezi documentation states that:
If you really feel attached to your thin, unicorn, puma or passanger server, you can still integrate Plezi with your existing application, but they won't be able to share the same process and you will need to utilize the Placebo API (a guide is coming soon).
But the guide isn't written yet...
There's some information in the GitHub Readme, but it will be removed after the guide is written.
Basically you include the Plezi application with the Redis URL inside your Rails application (remember to make sure to copy all the gems used in the gemfile). than you add this line:
Plezi.start_placebo
That should be it.
Plezi will ignore the Plezi.start_placebo command if there is no other server defined, so you can put the comment in a file shared with the Rails application as long as Plezi's gem file doesn't have a different server.
You can include some or all of the Rails application code inside the Plezi application. As long as Plezi (Iodine, actually) is the only server in the Plezi GEMFILE, it should work.
The applications will synchronize using Redis and you can use your Plezi code to broadcast websocket events inside your Rails application.
You may want to have a look at https://github.com/rails/actioncable which is the Rails way to deal with WebSockets, but currently in Alpha.
Judging from your code snippet, the client seems to only consume data from your backend. I'm skeptical whether you really need WebSockets. Ιf the client won't push data back to the server, Server Sent Events seem more appropriate.
See relevant walk-through and documentation.

Passing variables to config/environments/demo.rb from the Rails app

I have been struggling with a problem for the past days in a Ruby on Rails App I'm currently working on. I have different countries and for each country we use different Amazon S3 buckets. Amazon S3 key credentials are stored as constants in config/environments/environment_name.rb(ex:demo.rb) There is no way for me to determine which country we are operating from the config file. I can determine which country we are operating from the controllers,models,views,etc but not from the config file. Is there a Ruby meta programming or some other kind of magic that I'm not aware of so that I want to say if we are working on UK as a country in the app, use UK's bucket credentials or Germany as a country, use Germany's bucket credentials? I can't think of a way to pass parameters to environment files from the app itself. Thank you very much in advance for all your helps.
Rather than actually pass the configuration details to whichever S3 client you're using at launch, you should probably select the relevant credentials for each request. Your config file can define them all in a hash like so:
# config/s3.rb
S3_BUCKETS => {
:us => 'our-files-us',
:gb => 'our-files-gb',
:tz => 'special-case'
}
Then you can select the credentials on request like so (in maybe your AppController):
bucket_name = S3_BUCKETS[I18n.locale]
# pass this info to your S3 client
Make sense?
Write a little middleware if you want to keep the knowledge of the per-country configuration out of the main application.
A middleware is extremely simple. A do-nothing middleware looks like this:
class DoesNothing
def initialize(app, *args)
#app = app
end
def call(env)
#app.call(env)
end
end
Rack powers applications through chaining a series of middlewares together... each one is given a reference to #app, which is the next link in the chain, and it must invoke #call on that application. The one at the end of the chain runs the app.
So in your case, you can do some additional configuration in here.
class PerCountryConfiguration
def initialize(app)
#app = app
end
def call(env)
case env["COUNTRY"]
when "AU"
Rails.application.config.s3_buckets = { ... }
when "US"
Rails.application.config.s3_buckets = { ... }
... etc
end
#app.call(env)
end
end
There are several ways to use the middleware, but since it depends on access to the Rails environment, you'll want to do it from inside Rails. Put it in your application.rb:
config.middleware.use PerCountryConfiguration
If you want to pass additional arguments to the constructor of your middleware, just list them after the class name:
config.middleware.use PerCountryConfiguration, :some_argument
You can also mount the middleware from inside of ApplicationController, which means all of the initializers and everything will have already been executed, so it may be too far along the chain.

Secure paperclip urls only for secure pages

I'm trying to find the best way to make paperclip urls secure, but only for secure pages.
For instance, the homepage, which shows images stored in S3, is http://mydomain.com and the image url is http://s3.amazonaws.com/mydomainphotos/89/thisimage.JPG?1284314856.
I have secure pages like https://mydomain.com/users/my_stuff/49 that has images stored in S3, but the S3 protocol is http and not https, so the user gets a warning from the browser saying that some elements on the page are not secure, blah blah blah.
I know that I can specify :s3_protocol in the model, but this makes everything secure even when it isn't necessary. So, I'm looking for the best way to change the protocol to https on the fly, only for secure pages.
One (probably bad) way would be to create a new url method like:
def custom_url(style = default_style, ssl = false)
ssl ? self.url(style).gsub('http', 'https') : self.url(style)
end
One thing to note is that I'm using the ssl_requirement plugin, so there might be a way to tie it in with that.
I'm sure there is some simple, standard way to do this that I'm overlooking, but I can't seem to find it.
If anyone stumbles upon this now: There is a solution in Paperclip since April 2012! Simply write:
Paperclip::Attachment.default_options[:s3_protocol] = ""
in an initializer or use the s3_protocol option inside your model.
Thanks to #Thomas Watson for initiating this.
If using Rails 2.3.x or newer, you can use Rails middleware to filter the response before sending it back to the user. This way you can detect if the current request is an HTTPS request and modify the calls to s3.amazonaws.com accordingly.
Create a new file called paperclip_s3_url_rewriter.rb and place it inside a directory that's loaded when the server starts. The lib direcotry will work, but many prefer to create an app/middleware directory and add this to the Rails application load path.
Add the following class to the new file:
class PaperclipS3UrlRewriter
def initialize(app)
#app = app
end
def call(env)
status, headers, response = #app.call(env)
if response.is_a?(ActionController::Response) && response.request.protocol == 'https://' && headers["Content-Type"].include?("text/html")
body = response.body.gsub('http://s3.amazonaws.com', 'https://s3.amazonaws.com')
headers["Content-Length"] = body.length.to_s
[status, headers, body]
else
[status, headers, response]
end
end
end
Then just register the new middleware:
Rails 2.3.x: Add the line below to environment.rb in the beginning of the Rails::Initializer.run block.
Rails 3.x: Add the line below to application.rb in the beginning of the Application class.
config.middleware.use "PaperclipS3UrlRewriter"
UPDATE:
I just edited my answer and added a check for response.is_a?(ActionController::Response) in the if statement. In some cases (maybe caching related) the response object is an empty array(?) and hence fails when request is called upon it.
UPDATE 2:
I edited the Rack/Middleware code example above to also update the Content-Length header. Otherwise the HTML body will be truncated by most browsers.
Use the following code in a controller class:
# locals/arguments/methods you must define or have available:
# attachment - the paperclip attachment object, not the ActiveRecord object
# request - the Rack/ActionController request
AWS::S3::S3Object.url_for \
attachment.path,
attachment.options[:bucket].to_s,
:expires_in => 10.minutes, # only necessary for private buckets
:use_ssl => request.ssl?
You can of course wrap this up nicely into a method.
FYI - some of the answers above do not work with Rails 3+, because ActionController::Response has been deprecated. Use the following:
class PaperclipS3UrlRewriter
def initialize(app)
#app = app
end
def call(env)
status, headers, response = #app.call(env)
if response.is_a?(ActionDispatch::BodyProxy) && headers && headers.has_key?("Content-Type") && headers["Content-Type"].include?("text/html")
body_string = response.body[0]
response.body[0] = body_string.gsub('http://s3.amazonaws.com', 'https://s3.amazonaws.com')
headers["Content-Length"] = body_string.length.to_s
[status, headers, response]
else
[status, headers, response]
end
end
end
And make sure that you add the middleware in a good place in the stack (I added it after Rack::Runtime)
config.middleware.insert_after Rack::Runtime, "PaperclipS3UrlRewriter"

Support for multiple domains/subdomains in Rails

I have a Rails app that has a similar setup to Tumblr, that is, you can have either:
(1) Subdomain hosting (your-username.myapp.com)
(2) Domain hosting (your-username.com)
Both would forward to a personalized website for that user, created with my application.
How can I accomplish this in Rails? I have been able to get (1) working with subdomain-fu, but I'm not sure how to get (2) working. Any pointers (plugins, gems, tutorials), etc. would be greatly helpful, I can't seem to find any.
Thanks!
The principle for domains is the same as the subdomain - find the domain, map to an account.
The details will depend on how your hosting is going to handle the DNS.
I am currently using Heroku and its wildcard service.
In this case, the domain is mapped with a cname to the subdomain hosted by my Heroku app. From here I can work out the associated account and details.
EDIT: I've found a much easier way: http://www.arctickiwi.com/blog/7-host-and-domain-based-routing-in-ruby-on-rails
Not exactly an answer but this is the best I can give. Maybe this'll help you too.
Ideally, this blog post from transfs.com and subdomain-fu should do the trick. I've been trying to implement it, however, and they don't seem to play nicely together.
Basically, if I don't include the intiializer, the subdomain route works fine. If I include the initializer, the subdomain route breaks (everything gets caught by map.root). I have a feeling it's with the way it builds the condition string in the initializer. If you can figure out how it breaks, then you'll have a working app.
My initializer:
module ActionController
module Routing
class RouteSet
def extract_request_environment(request)
env = { :method => request.method }
env[:domain] = request.domain if request.domain
env[:host] = request.host if request.host
env
end
end
class Route
alias_method :old_recognition_conditions, :recognition_conditions
def recognition_conditions
result = old_recognition_conditions
[:host, :domain].each do |key|
if conditions[key]
operator = "==="
if conditions[key].is_a?(Regexp)
operator = "=~"
end
result << "conditions[:#{key.to_s}] #{operator} env[:#{key.to_s}]"
end
end
result
end
end# end class Route
end
end
My routes (just for development). You'll see my local development domain, stiltify.dev. Sorry, I tried to make it look good in here but I couldn't get the code block to look nice. I put it on pastie instead: http://pastie.org/940619.
The comments section in Ryan Bates' screencast was very helpful, and got me to figure out the subdomain => false and the other errors they were getting into. Still didn't fix the problem though!

Rails redirect with https

I'm maintaining a Ruby on Rails site and I'm confused as to how to perform redirects to relative URLs using the https protocol.
I can successfully create a redirect to a relative URL using http, for example:
redirect_to "/some_directory/"
But I cannot discern how to create a redirect to a URL using the https protocol. I have only been able to do so by using absolute URLS, for example:
redirect_to "https://mysite.com/some_directory/"
I would like to keep my code clean, and using relative URLs seems like a good idea. Does anyone know how to achieve this in Rails?
The ActionController::Base#redirect_to method takes an options hash, one of the parameters of which is :protocol which allows you to call:
redirect_to :protocol => 'https://',
:controller => 'some_controller',
:action => 'index'
See the definition for #redirect_to and #url_for for more info on the options.
Alternatively, and especially if SSL is to be used for all your controller actions, you could take a more declarative approach using a before_filter. In ApplicationController you could define the following method:
def redirect_to_https
redirect_to :protocol => "https://" unless (request.ssl? || request.local?)
end
You can then add filters in your those controllers which have actions requiring SSL, e.g:
class YourController
before_filter :redirect_to_https, :only => ["index", "show"]
end
Or, if you require SSL across your entire app, declare the filter in ApplicationController:
class ApplicationController
before_filter :redirect_to_https
end
If you want your entire application to be served over https then since Rails 4.0 the best way to do this is to enable force_ssl in the configuration file like so:
# config/environments/production.rb
Rails.application.configure do
# [..]
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
end
By default this option is already present in config/environments/production.rb in in newly generated apps, but is commented out.
As the comment says, this will not just redirect to https, but also sets the Strict-Transport-Security header (HSTS) and makes sure that the secure flag is set on all cookies. Both measures increase the security of your application without significant drawbacks. It uses ActionDispatch:SSL.
The HSTS expire settings are set to a year by default and doesn't include subdomains, which is probably fine for most applications. You can configure this with the hsts option:
config.hsts = {
expires: 1.month.to_i,
subdomains: false,
}
If you're running Rails 3 (>=3.1) or don't want to use https for the entire application, then you can use the force_ssl method in a controller:
class SecureController < ApplicationController
force_ssl
end
That's all. You can set it per controller, or in your ApplicationController. You can force https conditionally using the familiar if or unless options; for example:
# Only when we're not in development or tests
force_ssl unless: -> { Rails.env.in? ['development', 'test'] }
You're probably better off using ssl_requirement and not caring if a link or redirect is or isn't using https. With ssl_requirement, you declare which actions require SSL, which ones are capable of SSL and which ones are required not to use SSL.
If you're redirecting somewhere outside of your Rails app, then specifying the protocol as Olly suggests will work.
If you want to globally controll the protocol of urls generated in controllers, you can override the url_options method in you application controller. You could force the protocol of the generated urls depending on the rails env like so :
def url_options
super
#_url_options.dup.tap do |options|
options[:protocol] = Rails.env.production? ? "https://" : "http://"
options.freeze
end
end
this example works in rails 3.2.1, i'm not exactly sure for earlier or future versions.
This answer is somewhat tangential to the original question, but I record it in case others end up here in similar circumstances to myself.
I had a situation where I needed to have Rails use https proto in url helpers etc. even though the origin of all requests is unencrypted (http).
Now, ordinarily in this situation (which is normal when Rails is behind a reverse proxy or load balancer etc.), the x-forwarded-proto header is set by the reverse proxy or whatever, so even though requests are unencrypted between the proxy & rails (probably not advisable in production by the way) rails thinks everything is in https.
I needed to run behind an ngrok tls tunnel. I wanted to have ngrok terminate the tls with letsencrypt certificates I specified. However when it does so, ngrok does not offer the ability to customize headers, including setting x-forwarded-proto (although this feature is planned at some point in the future).
The solution turned out to be quite simple: Rails does not depend on either the protocol of the origin or whether x-forwarded-proto is set directly, but on the Rack env var rack.url_scheme. So I just needed to add this Rack middleware in development:
class ForceUrlScheme
def initialize(app)
#app = app
end
def call(env)
env['rack.url_scheme'] = 'https'
#app.call(env)
end
end
In Rails 4 one can use the force_ssl_redirect before_action to enforce ssl for a single controller. Please note that by using this method your cookies won't be marked as secure and HSTS is not used.
If you want to force ALL traffic via https, then the best way in Rails 6 is to configure production.rb with:
config.force_ssl = false
If you need a more flexible solution, you can handle it with a simple before_action filter:
class ApplicationController < ActionController::Base
include SessionsHelper
include LandingpageHelper
include ApplicationHelper
include UsersHelper
include OrganisationHelper
before_action :enforce_ssl, :except => [:health]
def enforce_ssl
if ENV['ENFORCE_SSL'].to_s.eql?('true') && !request.ssl?
redirect_to request.url.gsub(/http/i, "https")
end
end
end
If you run your application on AWS ECS Fargate with health checks, then you need a more flexible solution because the health check from the AWS target group is not invoked via https. Of course, you want the health check to work and at the same time, you want to force SSL for all other controller methods.
The ENFORCE_SSL is just an environment variable that turns this feature on/off.
Add protocol to ..._url:
redirect_to your_url(protocol: 'https')
or with subdomain:
redirect_to your_url(protocol: 'https', subdomain: 'your_subdomain')
Relative URLs, by definition, use the current protocol and host. If you want to change the protocol being used, you need to supply the absolute URL. I would take Justice's advice and create a method that does this for you:
def redirect_to_secure(relative_uri)
redirect_to "https://" + request.host + relative_uri
end
Open the class that has redirect_to and add a method redirect_to_secure_of with an appropriate implementation. Then call:
redirect_to_secure_of "/some_directory/"
Put this method in the lib directory or somewhere useful.

Resources