Authorizing websocket via token: connection vs channel - ruby-on-rails

A common way to handle authentication/authorization of websocket connections is to use cookie sent on initial http upgrade request. This is shown here http://edgeguides.rubyonrails.org/action_cable_overview.html
But what if I don't want to use cookie, but rather a token? Couple of hacks discussed here Send auth_token for authentication to ActionCable but it's too hacky imo.
What if I move the token authorization logic to channel:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :connection_id
def connect
self.connection_id = SecureRandom.hex # accept anyone, identify by random string
end
end
end
This will accept any connection. And than:
class TestChannel < ApplicationCable::Channel
def subscribed
if authorize(params[:token]) # check the token here
stream_from "test:1"
else
connection.close # alternatively `reject_subscription` to keep ws connection open
end
end
def unsubscribed
stop_all_streams
end
end
Frontend subscription will look like this
App.test = App.cable.subscriptions.create(channel: "TestChannel", token: "hello-world")
This seems to work. The individual connection is identified by a random string and each channel subscription holds this unique connection. When broadcast to this channel is made it will push the data to all subscribers through their unique connections. So if we rejected a subscriber here it won't receive anything. But we cannot close unauthorized connections before subscription attempt. I can just open the connection to /cable and hold it as long as I want.
But is this safe? The obvious problem here is that anybody can open as many ws connection as he like. Is this the only problem with such approach?

Related

Rails API 422 Unprocessable Entity: No verification key available, heroku

I created a Rails API with a JWT authentication system and deployed it to Heroku. When I request the endpoints locally, all seems to be working fine but when I make requests to the live endpoints (i.e the Heroku deployed app) I get a: 422 Unprocessable Entity server error and the response body looks like this:
{
"message": "No verification key available"
}
The class responsible for encoding and decoding the auth token is defined as follows:
class JsonWebToken
# secret to encode and decode token
HMAC_SECRET = Rails.application.secrets.secret_key_base
def self.encode(payload, exp = 24.hours.from_now)
# set expiry to 24 hours from the creation time.
payload[:exp] = exp.to_i
# sign token with application secret
JWT.encode(payload, HMAC_SECRET)
end
def self.decode(token)
# get payload, first index in decoded Array
body = JWT.decode(token, HMAC_SECRET)[0]
HashWithIndifferentAccess.new body
# rescue from all decode errors
rescue JWT::DecodeError => e
# raise custom error to be handled by custom handler
raise ExceptionHandler::InvalidToken, e.message
end
end
I have an endpoint /signup where I can make a POST request to register a new user and POST /todos which is accessible and available only to registered users. Making a registration request works perfectly fine, but when I try to make the POST request to the /todos endpoint it raises an error.
The association between user and suit is 1:m respectively.
Please if you have any idea on how I can fix this, I'll be very grateful, thanks : ).
I finally figured a way out by altering the Rails.application.secrets.secret_key_base to Rails.application.secret_key_base. For a more detailed review on this please check out this link. Hopefully, this will help someone facing a similar issue.
This was also my problem. After checking out my json_web_token.rb file, I figured out that I had written the following line:
HMAC_SECRET = Rails.application.secrets.secret_key_base
There is an extra secrets reference, which is causing the problem. It should be:
HMAC_SECRET = Rails.application.secret_key_base
But as far as I'm concerned, you managed to figure it out yourself!

Connecting ActionCable to different host

I'm running a rails 5 app as a backend server, and an ember application for a front-end application. They are two separate applications hosted on two different domains - say, backend.dev and frontend.dev
The rails application has a simple connection class found at app/channels/application_cable/connection.rb that looks like the following:
module ApplicationCable
class Connection < ActionCable::Connection::Base
def connect
Rails.logger.debug("env: #{env.inspect}")
Rails.logger.info("cookies.signed: #{cookies.signed.inspect}")
end
end
end
I have a simple base channel class at app/channels/application_cable/channel.rb with the following:
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
And a single implementation of that class at app/channels/events_channel.rb:
class EventsChannel < ApplicationCable::Channel
def subscribed
Rails.logger.debug("env: #{env.inspect}")
Rails.logger.info("cookies.signed: #{cookies.signed.inspect}")
stream_from 'events'
end
end
On the ember side of things, I'm using the ember-cable package. I've setup my consumer in my frontend by extending the controller class with the following:
cableService: Ember.inject.service('cable'),
setupConsumer: Ember.on('init', function() {
let service = this.get('cableService');
let consumer = service.createConsumer(`ws://backend.dev`);
let channel = 'EventsChannel';
consumer.subscriptions.create(channel, {
disconnected() {
Ember.debug(`${channel}#disconnected`);
},
connected() {
Ember.debug(`${channel}#connected`);
},
I'm fairly sure that my consumer is setup correctly, as I'm seeing some debug output when I get the following output to my js console:
DEBUG: EventsChannel#disconnected
However, I'm also seeing an odd error in the console as well:
WebSocket connection to 'ws://backend.dev/' failed: Error during WebSocket handshake: Unexpected response code: 200
I'm not sure what to make of the response code error here, and there's absolutely nothing being logged in my rails app. Is there anything additional that I need to setup to have actioncable work across domains? Any idea of what the 200 response code means here?
Try this:
# routes.rb
Rails.application.routes.draw do
# your code
mount ActionCable.server => '/cable'
end
Then in your app:
let consumer = service.createConsumer(`ws://backend.dev/cable`);
If you face handshake problems, there are few solutions:
Check if your frontend app is compatible with protocol 07 or newer.
Check if your website is in config.action_cable.allowed_request_origins
Add config.web_socket_server_url = 'ws://backend.dev/cable' to your ENV cofig file.
You can use a fast "dirty" hack. Just add following to your ENV cofig file:
config.action_cable.disable_request_forgery_protection = true

websocket-rails how to close socket connection?

I'm switching over from ActionController:Live to websocket-rails, and I was just wondering how to close the connection on the server side once the user closes the browser window?
With ActionController:Live I used to have:
def stream
response.headers['Content-Type'] = 'text/event-stream'
#redis_sub = RedisStream.new_redis_client
# Subscribing to user's stream by session token
#redis_sub.subscribe([ token ]) do |on|
on.message do |channel, msg|
## Did stuff
response.stream.write(msg)
end
end
rescue IOError
"\n\nIOError in controller"
rescue ClientDisconnected
puts "\n\nClient has disconnected\n\n"
ensure
#redis_sub.quit
response.stream.close
end
And this worked fine, now I'm trying to do the same thing as this but using websockets, and I was wondering how to close the connection and quit out of my redis subscription.
connection.close! from within the websocket controller.
https://github.com/websocket-rails/websocket-rails/issues/219
To deepen a little in #TheNastyOne answer, you can close socket a connection from dispatcher on client side like this:
// Open connection.
var dispatcher = new WebSocketRails('localhost:3000/websocket');
// Close connection.
dispatcher.disconnect();
Here lives the issue that describe both methods.

Ruby - Send message to a Websocket

How can I send data to a WebSocket using Ruby in a Background Process?
Background
I already have a separate ruby file running a Websocket server using the websocket-eventmachine-server gem. However, within my Rails application, I want to send data to the websocket in a background task.
Here is my WebSocket server:
EM.run do
trap('TERM') { stop }
trap('INT') { stop }
WebSocket::EventMachine::Server.start(host: options[:host], port: options[:port]) do |ws|
ws.onopen do
puts 'Client connected'
end
ws.onmessage do |msg, type|
ws.send msg, type: type
end
ws.onclose do
puts 'Client disconnected'
end
end
def stop
puts 'Terminating WebSocket Server'
EventMachine.stop
end
end
However, in my background process (I'm using Sidekiq), I'm not sure how to connect to the WebSocket and send data to it.
Here's my Sidekiq worker:
class MyWorker
include Sidekiq::Worker
def perform(command)
100.times do |i|
# Send 'I am on #{i}' to the Websocket
end
end
end
I was hoping to be able to do something like EventMachine::WebSocket.send 'My message!' but I don't see an API for that or something similar. What is the correct way to send data to a WebSocket in Ruby?
Accepted Answer:
Af your keeping your current websocket server:
You can use Iodine as a simple websocket client for testing. It runs background tasks using it's own reactor pattern based code and has a websocket client (I'm biased, I'm the author).
You could do something like this:
require 'iodine/http'
Iodine.protocol = :timers
# force Iodine to start immediately
Iodine.force_start!
options = {}
options[:on_message] = Proc.new {|data| puts data}
100.times do |i|
options[:on_open] = Proc.new {write "I am number #{i}"}
Iodine.run do
Iodine::Http.ws_connect('ws://localhost:3000', options)
end
end
P.S.
I would recommend using a framework, such as Plezi, for your websockets (I'm the author). Some frameworks let you run their code within a Rails/Sinatra app (Plezi does that and I think Faye, although not strictly a framework, does that too).
Using EM directly is quite hardcore and there are a lot of things to manage when dealing with Websockets, which a good framework helps you manage.
EDIT 3:
Iodine WebSocket client connections are (re)supported starting with Iodine 0.7.17, including TLS connections when OpenSSL >= 1.1.0.
The following code is an updated version of the original answer:
require 'iodine'
class MyClient
def on_open connection
connection.subscribe :updates
puts "Connected"
end
def on_message connection, data
puts data
end
def on_close connection
# auto-reconnect after 250ms.
puts "Connection lost, re-connecting in 250ms"
Iodine.run_after(250) { MyClient.connect }
end
def self.connect
Iodine.connect(url: "ws://localhost:3000/path", handler: MyClient.new)
end
end
Iodine.threads = 1
Iodine.defer { MyClient.connect if Iodine.master? }
Thread.new { Iodine.start }
100.times {|i| Iodine.publish :updates, "I am number #{i}" }
EDIT 2:
This answer in now outdated, since Iodine 0.2.x doesn't include a client any longer. Use Iodine 0.1.x or a different gem for websocket clients.
websocket-eventmachine-server is a websockets server.
If you want to connect to a websocket server using ruby, you can do it with some gems, like
https://github.com/igrigorik/em-websocket: Both server and client, also based on eventmachine.
ruby-websocket-client: Client only

Actionmailer SMTP Server Response

When sending mail through actionmailer, the actionmailer gets a response from the SMTP server, when its ok, or when its wrong. Is there a way to retrieve this response after sending a mail?
Also when no errors are thrown by the SMTP server?
Our qmail mail server throws a handler id which we want to use for tracing e-mails.
As an example, the server response is this :
250 ok 1308235825 qp 17832
Set return_response: true in the smtp settings and call message.deliver! instead of deliver. This returns the SMTP server response, a Net::SMTP::Response, which contains the server response you're looking for.
If you need a log of all responses from the connection with the server, not just the final result, you'll need to dig into Net::SMTP.
Looking at the the source you can define an observer:
in base.rb
# Register an Observer which will be notified when mail is delivered.
# Either a class or a string can be passed in as the Observer. If a string is passed in
# it will be <tt>constantize</tt>d.
def register_observer(observer)
delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
Mail.register_observer(delivery_observer)
end
So you could use some code like this in an initialization file:
class MailObserver
def self.delivered_email(message)
logger_info "Sent Message: #{message}"
end
end
ActionMailer::Base.register_observer(MailObserver)
That will log sent mail and you can see if you can get the headers or response from the sent mail object.

Resources