How to prevent spam when using websocket-rails gem? - ruby-on-rails

Consider the following to implement tic-tac-toe:
One player sends a move by triggering an event on the main dispatcher.
var dispatcher = new WebSocketRails('localhost:3000/websocket');
var channel = dispatcher.subscribe_private('private_game');
channel.bind('new_move', function(move) {
// received new move, process it
});
// later on when we want to send a move to the server we run the following code
var move = {
square: ...;
}
dispatcher.trigger('move', move);
On the server the controller can verify that the user is authorized for that specific game of tic-tac-toe. And then it can broadcast the move to both players.
class TicTacToeController < WebsocketRails::BaseController
def move
# code to verify the move is valid and save to database
...
# broadcast move to all players
WebsocketRails[:private_game].trigger(:new_move, message)
end
end
But there is nothing to enforce that the client sends messages only using the main dispatcher. The 'private_game' channel is suppose to be used only by the server for broadcasting moves. But a hostile client could send random data on it with
channel.trigger('new_move', randomdata);
Since channel events do not go through the Event Router and thus don't go through a Controller action, there is nothing on the server side to filter out the random spam.
Is there a way to stop random data spam on the server? Perhaps I'm misunderstanding how to use websocket-rails?

One way you could handle this now before the Gem is updated to support this better would be to use a separate private channel for each user.
Example
Game Starts
User A connects to private channel named user_a_incoming_moves
User B connects to private channel named user_b_incoming_moves
When user B makes a move, you broadcast that on the user_a_incoming_moves private channel that only User A is connected to.
When user A makes a move, you broadcast that on the user_b_incoming_moves channel that only User B is connected to.
This would prevent anyone from being able to send malicious moves through.
You can read more about private channels in the Private Channel Wiki.

Related

ActionCable display correct number of connected users (problems: multiple tabs, disconnects)

I'm a Rails beginner building a quiz webapp. Currently I'm using ActionCable.server.connections.length to display how many people are connected to my quiz, but there are multiple problems:
When the page is reloaded half of the time the old connection is not disconnected properly by ActionCable, so the number keeps rising even though it shouldn't
it only gives you the current number of connections for the specific thread this is called in, as #edwardmp pointed out in this actioncable-how-to-display-number-of-connected-users thread (which also means, that the number of connections displayed for the quiz hoster, which is one type of user in my app, can vary from the number of connections displayed to a quiz participant)
When a user connects with multiple browser windows each connection is counted seperately which falsely inflates the number of participants
And last but not least: it would be great to be able to display the number of people connected per room of my channel, instead of across all rooms
I've noticed that most answers concerning this topic use a Redis server, so I was wondering if that is generally recommended for what I'm trying to do and why. (e.g. here: Actioncable connected users list )
I currently use Devise and cookies for my authentication.
Any pointers or answers to even part of my questions would be greatly appreciated :)
I finally at least got it to work for counting all users to the server (not by room) by doing this:
CoffeeScript of my channel:
App.online_status = App.cable.subscriptions.create "OnlineStatusChannel",
connected: ->
# Called when the subscription is ready for use on the server
#update counter whenever a connection is established
App.online_status.update_students_counter()
disconnected: ->
# Called when the subscription has been terminated by the server
App.cable.subscriptions.remove(this)
#perform 'unsubscribed'
received: (data) ->
# Called when there's incoming data on the websocket for this channel
val = data.counter-1 #-1 since the user who calls this method is also counted, but we only want to count other users
#update "students_counter"-element in view:
$('#students_counter').text(val)
update_students_counter: ->
#perform 'update_students_counter'
Ruby backend of my channel:
class OnlineStatusChannel < ApplicationCable::Channel
def subscribed
#stream_from "specific_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
#update counter whenever a connection closes
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
def update_students_counter
ActionCable.server.broadcast(specific_channel, counter: count_unique_connections )
end
private:
#Counts all users connected to the ActionCable server
def count_unique_connections
connected_users = []
ActionCable.server.connections.each do |connection|
connected_users.push(connection.current_user.id)
end
return connected_users.uniq.length
end
end
And now it works! When a user connects, the counter increments, when a user closes his window or logs off it decrements. And when a user is logged on with more than 1 tab or window they are only counted once. :)

How can a Slack bot detect a direct message vs a message in a channel?

TL;DR: Via the Slack APIs, how can I differentiate between a message in a channel vs a direct message?
I have a working Slack bot using the RTM API, let's call it Edi. And it works great as long as all commands start with "#edi"; e.g. "#edi help". It currently responses to any channel it's a member of and direct messages. However, I'd like to update the bot so that when it's a direct message, there won't be a need to start a command with "#edi"; e.g. "#edi help" in a channel, but "help" in a direct message. I don't see anything specific to differentiate between the two, but I did try using the channel.info endpoint and counting the number of people in "members"; however, this method only works on public channel. For private channels and direct messages, the endpoint returns an "channel_not_found" error.
Thanks in advance.
I talked to James at Slack and he gave me a simply way to determine if a message is a DM or not; if a channel ID begins with a:
C, it's a public channel
D, it's a DM with the user
G, it's either a private channel or multi-person DM
However, these values aren't set in stone and could change at some point, or be added to.
So if that syntax goes away, another way to detect a DM to use both channels.info and groups.info. If they both return “false” for the “ok” field, then you know it’s a DM.
Note:
channels.info is for public channels only
groups.info is for private channels and multi-person DMs only
Bonus info:
Once you detect a that a message is a DM, use either the user ID or channel ID and search for it in the results of im.list; if you find it, then you’ll know it’s a DM to the bot.
“id” from im.list is the channel ID
“user” from im.list is the user ID from the person DM’ing with the bot
You don’t pass in the bot’s user ID, because it’s extracted from the token
FYI as of July 2017, for "message.im" events (via your app's Event Subscriptions), the event payload seems to now return additional fields to detect if the message is coming from your own bot (pasted in here from my logs):
INFO[0012] got Slack message: (bot.SlackMessage) {
SlackEvent: (bot.SlackEvent) {
Type: (string) (len=7) "message",
EventTs: (string) (len=17) "1501076832.063834",
User: (string) ""
},
SubType: (string) (len=11) "bot_message",
Channel: (string) (len=9) "D6CJWD132",
Text: (string) (len=20) "this is my bot reply",
Username: (string) (len=15) "Myapp Local",
BotID: (string) (len=9) "B6DAZKTGG",
Ts: (string) (len=17) "1501076832.063834"
}
Slack have added Conversations API some time ago. You should use it to differentiate between PM/channel instead of relying on prefix.
From Conversations API documentation:
Each channel has a unique-to-the-team ID that begins with a single letter prefix, either C, G, or D. When a channel is shared across teams (see Developing for Shared Channels), the prefix of the channel ID may be changed, e.g. a private channel with ID G0987654321 may become ID C0987654321.
This is one reason you should use the conversations methods instead of the previous API methods! You cannot rely on a private shared channel's unique ID remaining constant during its entire lifetime.
Get conversation info using conversations.info method and check is_im flag. is_im == true means that the conversation is a direct message between two distinguished individuals or a user and a bot.
The info function is also available for private channels with the Slack API method groups.info. This works also for direct message channels with multiple participants, since they are a special form of private channels.
You can use groups.list to get the IDs of all private channels incl. direct message channels with multiple participants.
Note that groups.list will only return private channels, that the user or bot that the access token belongs to has been invited to.

How to transmit initial payload to the client in ActionCable subscribed callback?

I have Ruby on Rails application which is using ActionCable for real-time client communication. I have ScoreChannel which is responsible for streaming user score updates:
class ScoreChannel < ApplicationCable::Channel
def self.broadcast_name(user:)
"score_#{user.guid}"
end
def broadcast_name
self.class.broadcast_name(user: current_user)
end
def subscribed
stream_from(broadcast_name)
ActionCable.server.broadcast(broadcast_name, Cable::V1::ScoreSerializer.new(current_user).as_json)
end
end
I am trying to send user's current score right after a user subscribes to the channel (see ScoreChannel#subscribed method). Unfortunately, due to asynchronous nature of ActionCable, score broadcast goes before user subscribes to the ScoreChannel. Therefore user does not receive the initial score payload (because he is not subscribed to this channel yet). This is redis monitor timeline:
1472430005.884742 [1 172.18.0.6:50242] "subscribe" "action_cable/Z2lkOi8vYXBwLWVuZ2luZS9Vc2VyLzE"
1472430010.988077 [1 172.18.0.6:50244] "publish" "score_cc112e3fdfb5411a965a31f9468abf98" "{\"score\":17,\"rank\":1}"
1472430010.988773 [1 172.18.0.6:50242] "subscribe" "score_cc112e3fdfb5411a965a31f9468abf98"
What is the best way to solve this?
Instead of using ActionCable.server.broadcast to broadcast to the existing subscribers, try using the transmit-method, like this:
transmit(Cable::V1::ScoreSerializer.new(current_user).as_json)
This only sends the status to the current subscriber being handled, but I figured this is what you wanted.
use transmit inside a channel class/instance, ref ActionCable::Channel::Base#transmit
use broadcast outside channel class, ref ActionCable::Server#broadcast

Rails and Websockets: need some direction on a feature

In my rails app a User has a profile and this profile can be publicly accessible. I have been looking at using the Pusher gem (https://github.com/pusher/pusher-gem) to use for plug-and-play websocket usage with my app.
Basically I am wanting it so that if a user is looking at a public profile and the owner of that profile happens to update that profile then the front-end is updated with the new information and the user is notified on the front-end about the update.
Any help on where to get started would be great!
For each public profile have a unique channel name e.g. <user_name>-profile.
Whenever an updated occurs on the user user_name's profile trigger an event on the user's channel, passing the updated data.
data = update_profile()
Pusher.trigger( '<user_name>-profile', 'profile-updated', {:profile => data} )
On the profile page running the the browser have the code that listens to updates only on the relevant channel:
var pusher = new Pusher( APP_KEY );
var channel = pusher.subscribe( '<user_name>-profile' );
channel.bind( 'profile-updated', function( update ) {
// Update UI to show new profile information
// Show something to indicate that an update has occurred
} );
The one problem here is that you will be triggering an event even when nobody is viewing the public profile. If you wanted to fix that you would need to use WebHooks and keep track of whether or not a profile channel was occupied and only trigger the event if it is.

How to keep track of a process per browser window and access it at each event in Nitrogen?

In Nitrogen, the Erlang web framework, I have the following problem. I have a process that takes care of sending and receiving messages to another process that acts as a hub. This process acts as the comet process to receive the messages and update the page.
The problem is that when the user process a button I get a call to event. How do I get ahold of that Pid at an event.
the code that initiates the communication and sets up the receiving part looks like this, first I have an event which starts the client process by calling wf:comet:
event(start_chat) ->
Client = wf:comet(fun() -> chat_client() end);
The code for the client process is the following, which gets and joins a room at the beginning and then goes into a loop sending and receiving messages to/from the room:
chat_client() ->
Room = room_provider:get_room(),
room:join(Room),
chat_client(Room).
chat_client(Room) ->
receive
{send_message, Message} ->
room:send_message(Room, Message);
{message, From, Message} ->
wf:insert_bottom(messages, [#p{}, #span { text=Message }]),
wf:comet_flush()
end,
chat_client(Room).
Now, here's the problem. I have another event, send_message:
event(send_message) ->
Message = wf:q(message),
ClientPid ! {send_message, Message}.
except that ClientPid is not defined there, and I can't see how to get ahold of it. Any ideas?
The related threat at the Nitrogen mailing list: http://groups.google.com/group/nitrogenweb/browse_thread/thread/c6d9927467e2a51a
Nitrogen provides a key-value storage per page instance called state. From the documentation:
Retrieve a page state value stored under the specified key. Page State is different from Session State in that Page State is scoped to a series of requests by one user to one Nitrogen Page:
wf:state(Key) -> Value
Store a page state variable for the current user. Page State is different from Session State in that Page State is scoped to a series of requests by one user to one Nitrogen Page:
wf:state(Key, Value) -> ok
Clear a user's page state:
wf:clear_state() -> ok
Have an ets table which maps session id's to client Pid's. Or if nitrogen provides any sort of session management, store the Pid as session data.
Every thing that needs to be remembered needs a process. It looks like your room provider isn't.
room:join(Room) need to be room:join(Room,self()). The room need to know what your comet-process pid is.
To send a message to a client you first send the message to the room, the room will then send a message to all clients in the room. But for that to work. Every client joining the room need to submit the comet-pid. The room need to keep a list of all pid's in the room.

Resources