Reading Rails 4.1 session cookie in Rails 6 - ruby-on-rails

I’m attempting to strangle a legacy Rails 4.1 application by using a proxy in front of it and sending some requests to a new Rails 6 application.
The Rails 4 application is responsible for user authentication and setting the initial session cookie.
Is there a way to get the Rails 6 application to read/write Rails 4.1 compatible session cookies?
Thanks

I wanted to write an answer here as I recently had to dig through some Rails guts to solve this issue. First, make sure both apps share the same secret_key_base, cookie domain, and cookie key. This will let the apps at least read the same session cookie:
MyApp::Application.config.session_store :cookie_store, {
domain: 'myapp.com', # can omit if they're on the same domain
key: '_myapp_session', # need to be explicit here
expire_after: 60.minutes
}
Then, you need to make sure both apps can read the actual payload. The default cookie serialization is moving to JSON in Rails 7, but any older version will still default to :marshal (which uses the basic Ruby Marshal module). Make sure you're using the same serializer.
Rails.application.config.action_dispatch.cookies_serializer = :marshal
Lastly, if your newer app is Rails 6.2+, be warned that Rails started wrapping all cookie payloads (regardless of serialization method) in a JSON envelope. This envelope can't be opened by Rails 4.2 or older apps. So your session can move from Rails 4.2 -> 6.2, but not back...
Unless you monkeypatch it with a custom deserializer, like so:
class WrappedSerializer
def self.base_serializer
Marshal # or JSON, if you're using :json above
end
def self.load(value)
# if this is a Rails 4-created session, parsing will fail & hit the rescue
json = JSON.parse(value) rescue nil
return base_serializer.load(value) if json.nil? || !json.key?('_rails')
base_serializer.load(Base64.decode64(json.dig('_rails', 'message')))
end
def self.dump(value)
base_serializer.dump(value)
end
end
module ActionDispatch
class Cookies
module SerializedCookieJars
def serializer
WrappedSerializer
end
end
end
end
I should point out that, because this strips the envelope which might contain a custom expiry time, the Rails 5.2+ feature of being able to set custom expiry times will not work for any sessions that transition back to your Rails 4 app. If you use that feature, you could probably find a way change the above serializer to rewrap the payload when it deserializes.

Related

when sharing data via memcached between Rails and Sinatra, how fix Sinatra not handling ActiveSupport

Is there any way to cause Sinatra to transparently handle cached data correctly that was written by Rails, eg, to implicitly handle the ActiveSupport class that Rails uses when it stores data?
A Heroku-hosted Rails 4 app and a Sinatra app used a shared Memcachier store to share certain ephemeral data.
If a key/value is created on Sinatra, everything works normally:
# on Sinatra:
session.cache.set("foo", "from sinatra", 100)
will set a key that either the Sinatra app or the Rails app can read, and either app will automatically read nil once it expires in 100 seconds. And both Rails and Sinatra report the class of the data value to be String.
However, if the data is set by the Rails app:
# on Rails:
Rails.cache.write("foo", "from rails", expires_in: 100)
the Rails app read returns a String (as expected), but Sinatra app get returns class ActiveSupport::Cache::Entry
# on Sinatra
d = settings.cache.get("foo")
=> #<ActiveSupport::Cache::Entry:0x7f7ea70 #value="from rails", #created_at=1598330468.8312092, #expires_in=100.0>
and if the Sinatra app fetches the same key after the expiry, the same data is returned (not nil).
It is certainly possible on the Sinatra app to brute-force it with a new method that get's the data, and manually handles expiration and data issues using ActiveSupport::Cache::Entry methods, as outlined below.
But it seems like there ought to be some way to have the ActiveSupport::Cache::Entry code handle those details automatically by telling Sinatra to use ActiveSupport::Cache for both setting and getting data?
# pseudo code for Sinatra app
def new_get(key)
x = settings.cache.get(key)
if x.class == ActiveSupport::Cache::Entry
if x.expired?
x.delete
return nil
else return x.value
else return x
end
Currently the memcache store is configured as per Heroku online documentation:
require 'dalli'
set :cache, Dalli::Client.new(
(ENV["MEMCACHIER_SERVERS"] || "").split(","),
{:username => ENV["MEMCACHIER_USERNAME"],
:password => ENV["MEMCACHIER_PASSWORD"],
:failover => true, # default is true
:socket_timeout => 1.5, # default is 0.5
:socket_failure_delay => 0.2, # default is 0.01
:down_retry_delay => 60 # default is 60
})
EDIT - SOLVED Per accepted answer the key to Sinatra/Rails data interoperability was to explicitly configure the cache store to use ActiveSupport, which still automagically uses the Dalli gem to manage the connection to the memcachier service.
set :cache, ActiveSupport::Cache::MemCacheStore.new(
... # init parameters
)}
which also means using the settings.cache.read/write methods (vs the settings.cache.get/set methods used when you configure using Dalli::Client.new).
ADDITIONAL EDIT:
When using the cache inside a model, you cannot access settings.cache directly, need to use Sinatra::Application.settings.cache.read()
I am not a Sinatra user, Used something like the below in Rails.
cache_store = ActiveSupport::Cache::MemCacheStore.new('localhost')
# For writing the keys:
cache_store.write("foo", "from rails", expires_in: 100)
# For reading the keys
cache_store.read("foo")
EDIT
It seems you are using gem dalli, You can also use the DalliStore instead of MemCacheStore like the below:
cache_store = ActiveSupport::Cache::DalliStore.new('localhost')
# For writing the keys:
cache_store.write("foo", "from rails", expires_in: 100)
# For reading the keys
cache_store.read("foo")

Rails 6.0.2.1 - “sameSite” attribute set to “none” “secure” attribute

Firefox error:
Cookie “_myapp_session” will be soon rejected because it has the
“sameSite” attribute set to “none” or an invalid value, without the
“secure” attribute. To know more about the “sameSite“ attribute, read
https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite
"To fix this, you will have to add the Secure attribute to your SameSite=None cookies."
How do I add the secure attribute into my SameSite=None cookie, when using Rails 6?
I do not want to add a separate gem to accomplish this.This error randomly appeared, I assume there was a browser change. Does rails 6 have a native way to fix this? I read this post,
Thank you
You can configure your session store to use secure cookies in production, just add this to an initializer:
MyApp::Application.config.session_store :cookie_store, key: '_my_app_session', secure: Rails.env.production?
You may already have it on config/initializers/session_store.rb.
Documentation and pertinent issue. This will be fixed in Rails 6.1.
You need this line in your Rails config file:
# Specify cookies SameSite protection level: either :none, :lax, or :strict.
#
# This change is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 6.1.
Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
Update to rails 6.1 (see documentation here on how to do that)
Add the following line to config/application.rb (see the doc here for details on the cookies_same_site_protection option):
# config/application.rb
...
module YouAppName
class Application < Rails::Application
...
# Specify cookies SameSite protection level: either :none, :lax, or :strict.
# This change is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 6.1.
# Was not in Rails 6.0. Default in rails 6.1 is :lax, not :strict
config.action_dispatch.cookies_same_site_protection = :strict
...
end
end
This line could also be added to config/environments/development.rb, config/environments/production.rb or to an initializer depending on your needs.

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.

HTTP request uuid or request start time in Ruby applications

I need to get a request uuid or time when server get a request. It's easy in Rails, but I'm working on a gem and I would like it to be more generic. So I would like it to work also with Sinatra and every other Ruby application which works in a http server.
This is another problem, it's a gem. I can't put Time.now at the beggining of my application controller. I need it to be generic, so it should work with different frameworks.
What would you propose?
You can implement a Rack middleware which you can use independently from your actual application framework (as long as it used rack, which is true for at least Rails, Sinatra, Padriono and most other Ruby web frameworks).
Rails already includes a middleware for adding a unique ID to a request of required in ActionDispatch::RequestId. Another alternative could be the rack-request-id gem.
A minimal versions of this midleware could look like this:
class RequestIdMiddleware
def initialize(app)
#app = app
end
def call(env)
env['request_id'] = env['HTTP_X_REQUEST_ID'] || SecureRandom.uuid
env['request_started_at'] = Time.now
#app.call(env)
end
end
You can then use this middleware in your config.ru or by adding this to your application.rb in Rails:
config.middleware.use RequestIdMiddleware

Rails 3 sessions not lazy loading

I've read that sessions in Rails 3 are lazy loaded, but I'm not seeing that behavior. To test this, I created a new Rails 3.2 app using MySQL and the activerecord session store. No gems added except the V8 JavaScript engine. Then I created an empty controller called welcome with an index action. No code in here at all. When I hit the index page, I see a session created in the sessions table in MySQL.
Am I doing something wrong? I thought a session would only be created if I accessed it.
That's a default behavior of Ruby on Rails beginning from version 3.0 I guess. See:
http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
If you need to stop sessions being created in the database for bots/crawlers, this is what worked for me:
# Save this file as config/initializers/session_store_ext.rb
# and don't forget to define BOT_REGEX to match bots/crawlers
class ActiveRecord::SessionStore
_set_session = instance_method :set_session
define_method :set_session do | env, sid, session_data, options |
unless env['HTTP_USER_AGENT'] =~ BOT_REGEX
_set_session.bind(self).call env, sid, session_data, options
end
sid
end
private :set_session
end
I've written a blog post explaining how I created this and why it works - Conditionally Disabling Database Sessions in Ruby on Rails 3

Resources