access Context.ConnectionID outside of hub in SignalR - asp.net-core

I want to be able to send message to specific user account with SignalR.
Right now I can send to everyone!
I am looking for any way to have access to context.ConnectionID in controller outside of hub. I tried accessing it inside of controller with iHubContext and inside of hub I tried to save value of connectionID in session but both are not supported.
What would be best way to access connectionID from Controller outside of Hub?
here is action method from ChatController that is used for sending message to everyone
public IActionResult PosaljiPoruku()
{
_hubContext.Clients.All.SendAsync("PrimljenaPoruka", "aaa");
return PartialView("SubmitFormPartial");
}
Any help is appreciated!
Thanks for trying to help!

What would be best way to access connectionID from Controller outside of Hub?
From this doc about sending messages from outside a hub, you would find:
When hub methods are called from outside of the Hub class, there's no caller associated with the invocation. Therefore, there's no access to the ConnectionId, Caller, and Others properties.
I want to be able to send message to specific user account with SignalR.
You can mapping user and ConnectionId(s) while client connected to Hub server, then if you want to send message to specific user from controller action outside of your Hub, you can query mapping table to get corresponding connection Id(s) first based on user name etc, and passing connection Id(s) to action method.
await _hubContext.Clients.Client("connection_id_here").SendAsync("PrimljenaPoruka", "aaa");
Besides, you can achieve same by creating single-user group (a group for each user), and then send a message to that group when you want to reach only that specific user.
await _hubContext.Clients.Group("group_name_here").SendAsync("PrimljenaPoruka", "aaa");

Related

How to filter data based on client permissions instead of broadcasting?

I am learning SignalR. Something I can't wrap my head around is this:
SignalR allows us to apply authentication/authorization using [Authorize] attribute. This works to prevent clients from calling a certain method on hub. However, how to make this work other way around? How do I ensure that hub pushes notifications to only those clients which are authorized to see the updates for specific set of data?
For example:
An Admin client updates a record. SignalR should push this update to other admin clients. However, doing Clients.All.SendAsync will push update to all of the clients. Non-admin clients shouldn't get a notification.
Any idea about how to achieve this?
You should add a RoleManager and then filter Clients.All by Id in other to get only a reduced list of admins.
var idsWithPermission = roleManager.FindByName("Admins").Users.Select(iur => iur.Id);
var clients = Clients.AllExcept.Where(u =>! idsWithPermission.Contains(u.Id))/*.SomeHelper()*/;
clients.Send("hello world");

Get SignalR User (Hub.Context) outside of hub

is there any way to get current signalR request user outside the hub? I can use Hub.Context.User inside of hub methods, but if hub method calls any other underlying layer? Wcf service call - an additional BehaviorExtensionElement is used to add wcf message header with current user identity name.
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
request.Headers.Add(MessageHeader.CreateHeader(
Constants.HeaderNames.SessionId,
Constants.HeaderNames.HeaderNamespace,
_hubManager.ResolveHub(Constants.Hubs.MessengerHub).
Context.User.Identity.Name));
}
Yes, i found that the DefaultHubManager gets the Hub, but i'm not sure it will be the hub from current request, not the concurrent one or a new one, 'cause at the end of ResolveHub i see the following code runs
return (DependencyResolverExtensions.Resolve(this._resolver, descriptor.HubType)
?? Activator.CreateInstance(descriptor.HubType)) as IHub;
Obviuosly i can pass user from hub method to wcf call but it requires refactoring to move from wcf behaviour to setting request field with user name explicitly.
Also can i rely on HttpContext.Current to get the info from cookie?
No you cannot. The only way to retrieve the currently active user outside of the hub itself would be to pass the users information to whatever method you call in the hub.
A common pattern is to track your users by adding them to some sort of dictionary in OnConnected and removing them in OnDisconnected. You can then have an entirely separate way of identifying your users while having required information that's associated with them.

Can callback channels be spoofed?

When users first connect to my sessionful service, they provide a user name in the form of a string. If multiple users have provided the same user name, the server appends all new users with a number that increments based on how many exist on the service already.
These user names are the value in a Dictionary<IChatCallback, string> dictionary. Whenever a user calls a method such as SendMessage on the server, the server detects who the caller is by doing a dictionary lookup based on OperationContext.Current.GetCallbackChannel<IChatCallback>() as the key.
My question is... can this channel be spoofed? Can somebody somehow call methods on the service under someone elses callback channel, and the server is oblivious?

WCF - how to write a publisher\subscriber service that publishes only to specific clients?

I am writing a WCF service in a Publish-Subscribe pattern.
When someone publishes an event, I don't want to straight away send it to all the clients.
I want to be able to, for each client, check if that client needs to be notified about that publish.
Basically this will be done by accessing a database, and checking if that client has subscribed for that specific event with those parameters (cannot be done in advance, needs to be checked only against database).
Currently I am working using this List-Based Publish-Subscriber sample, but it works in such a way - that when an event is published - client session is triggered separatly to send the message.
So for now, I am changing this :
public void PriceChangeHandler(object sender, PriceChangeEventArgs e)
{
_callback.PriceChange(e.Item, e.Price, e.Change);
}
to this :
public void PriceChangeHandler(object sender, PriceChangeEventArgs e)
{
// Perform some database checks using BL if this client needs to be notified about this event
// Only if answer is YES - call the callback function on that client
_callback.PriceChange(e.Item, e.Price, e.Change);
// Also - send the client an EMAIL + SMS
_emailServer.SendEmail(e.Item);
_smsServer.SendSMS(e.Item);
}
Two Questions :
Is this the right way ? and how can I know what 'this' client is ? should the client send me credentials in the 'subscribe' method that I will store ?
Or should I implement a custom 'UsernameValidator' that will store the Principal ?
And shouldn't I have a static list of all the clients, that I will send to my BL, and the BL will return me only the ones I have to send the message to ?
I think answering this question first will make life a whole lot easier:
and how can I know what 'this' client is ?
OperationContext.Current.GetCallbackChannel<T>
For each call the service receives there will be a client channel through which the call is made, this will give you the callback channel of the client that made that call only, this is a simple way in which you're able to distinguish your clients.
Regarding the approach to your scenario as a whole, I would first store a list of subscribers in a static dictionary as you suggested yourself, but also keep each clients callback instance along with their username:
private static Dictionary<IPriceChangeCallback, string> subscribers = new Dictionary<IPriceChangeCallback, string>();
Where IPriceChangeCallback is your callback contract and the string could be a unique Username or any identifier. So you now have the basic ability to distinguish your clients, for example say you want to publish the last received message to every client except the one who sent it, you would:
lock (subscribers)
{
foreach (var _subscriber in subscribers)
{
if (OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>() == _subscriber.Key)
{
//if the person who sent the last message is the current subscriber, there is no need to
//publish the message to him, so skip this iteration
continue;
}
else
{
//GetCurrrentClient is a handy method, you can optionally include this
//in your callbacks just to let your clients know who exactly sent the publication
_subscriber.Key.PriceChangeCallback(e.Item, e.Price, e.Change, GetCurrentClient());
}
}
}
or distinguish your clients based on their usernames, which you should ideally have in your databse as well:
lock (subscribers)
{
foreach (var _subscriber in subscribers)
{
if(_subscriber.Value == "Jimmy86"))
{
//Identify a specific client by their username and don't send the notification to him
//here we send the notification to everyone but jimmy86
continue;
}
else
{
_subscriber.Key.PriceChangeCallback(e.Item, e.Price, e.Change, GetCurrentClient());
}
}
}
And again, whenever you want to find out who called the service operation, and tell your clients who sent that particular message, use the GetCurrentClient() method I mentioned earlier:
private string GetCurrentClient()
{
return clients[OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>()];
}
Is this the right way ?
I'm not sure how advisable the approach above is, but I've done it before whenever I've wanted to keep a list of clients and call some method on them.
should the client send me credentials in the 'subscribe' method that I will store ?
Yes this is one common way of doing it. Have a Subscribe() operation on your service, which will be the first method your clients will call when they want to join your service:
[OperationContract(IsOneWay = true)]
public void Subscribe(string username)
{
lock (subscribers)
{
subscribers.Add(OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>(), username);
}
}
I was working on a Pub/Sub Silverlight service a couple months ago, and I found this article and it's accompanying video to be invaluable.
The answer I have come up with is to implement the 'Custom UsernamePasswordValidator',
and so each service instance now KNOWS what client is connected to it (this way I don't have to pass anything in Subscribe).
When a 'publish' event arrives - I would check which user it is intended to (the same user might connect from several machines).
I would then raise a 'PriceChangeEvent' with the targeted user, and the 'PriceChangeHandler' event would be raised for all client instances.
Then, inside the event - I would check if the logged principal is the targeted user, and if so - I would call the callback function on the client machine.
This saves me the trouble of saving a list of connected clients, and also I don't need to pass anything in the 'Subscribe' method.

How do I do username/password authentication in WCF, with session affinity?

It seems like I'm barking up the wrong tree when asking this question, this question and this question.
I need to authenticate users against a custom API (in COM), and I need to keep that custom API (the COM object) alive (for that user) for future WCF calls. During authentication against that custom API, I can get back a list of custom-defined roles. I'd also like to use these for authorization of the service methods.
Moreover, I need to be able to revoke the user's session remotely. This is triggered by an event raised by the COM API.
I've got a custom UserNamePasswordValidator, but it appears that this has no mechanism for correctly setting a custom principal, so it looks like I'm heading in the wrong direction.
How do I do these three things?
You can handle authentication completely in your service. Create service contract similar to:
[ServiceContract(SessionMode=SessionMode.Required)]
public interface IService
{
// All your operations marked with [OperationContract(IsInitiating=false, IsTerminating=false)]
// Two additional operations
[OperationContract(IsInitiating=true, IsTerminating=false)]
void Login(string user, string password);
[OperationContract(IsInitiating=false, IsTerminating=true)]
void Logout();
}
Service implementing this contract has to have PerSession instancing. Implement authentication in Login method and store COM object in local field. When new client want to use such service he has to first call the Login method. So all your instances will be properly authenticated and they will store their instance of COM object.
You can also register InstanceContext and COM object to some global class which will deal with forcibly killing service instance. This will probably require some research to make it work.
Make sure that you use some secure binding (encryption) because you will send user name and password as a plain text.