SignalR C# MVC Mapping Anonymous User to Client ID - c#

Question: How do I manage anonymous users so that multiple tabs in a single browser are all updated when the Hub sends out a response?
The scenario is as follows:
I would like to integrate SignalR into a project so that anonymous users can live chat with operators. Obviously user's that have authenticated with iIdentity are mapped via the Client.User(username) command. But currently say an anonymous user is browsing site.com/tools and site.com/notTools I can not send messages to all tabs with only a connectionID. Only one tab gathers the response.
I have tried using IWC patch but that tool doesn't account for saving chat information into a database and I think passing variables via ajax isn't a secure way to read/write to a database.
I have also looked in the following: Managing SignalR connections for Anonymous user
However creating a base such as that and using sessions seems less secure than Owin.
I had the idea to use client.user() by creating a false account each time a user connects and delete it when they disconnect. But I would rather not fill my aspnetusers db context with garbage accounts it seems unnecessary.
Is it possible to use UserHandler.ConnectedIds.Add(Context.ConnectionId); to also map a fake username? Im at a loss.
Would it make sense to use iUserId provider?
public interface IUserIdProvider
{
string GetUserId(IRequest request);
}
Then create a database that stores IP addresses to connectionIDs and single usernames?
database:
Users: With FK to connection IDs
|userid|username|IP |connectionIDs|
| 1 | user1 |127.0.0.1| 1 |
| 2 | user2 |127.0.0.2| 2 |
connectionIDs:
|connectionID|SignalRID|connectionIDs|
| 1 | xx.x.x.x| 1 |
| 2 | xx.xxx.x| 2 |
| 3 | xx.xxxxx| 2 |
| 4 | xxxxx.xx| 2 |
Then Possibly write logic around the connection?
public override Task OnConnected()
{
if(Context.Identity.UserName != null){
//add a connection based on the currently logged in user.
}
else{
///save new user to database?
}
}
but the question still remains how would I handle multiple tabs with that when sending a command on the websocket?
update
To be clear, my intent is to create a live chat/support tool that allows for in browser anonymous access at all times.
The client wants something similar to http://www.livechatinc.com
I have already created a javascript plugin that sends and receives from the hub, regardless of what domain it is on. (my client has multisites) the missing piece to the puzzle is the simplest, managing the anonymous users to allow for multi-tabbed conversations.

I'm not following the false user account idea (don't know if it works), but will develop an alternative.
The goal could be achieved through a ChatSessionId cookie shared by all browser tabs and creating Groups named as this ChatSessionId.
I took basic chat tutorial from asp.net/signalr and added functionality to allow chat from multiple tabs as the same user.
1) Assign a Chat Session Id in "Chat" action to identify the user, as we don't have user credential:
public ActionResult Chat()
{
ChatSessionHelper.SetChatSessionCookie();
return View();
}
2) Subscribe to chat session when enter the chat page
client side
$.connection.hub.start()
.then(function(){chat.server.joinChatSession();})
.done(function() {
...
server side (hub)
public Task JoinChatSession()
{
//get user SessionId to allow use multiple tabs
var sessionId = ChatSessionHelper.GetChatSessionId();
if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");
return Groups.Add(Context.ConnectionId, sessionId);
}
3) broadcast messages to user's chat session
public void Send(string message)
{
//get user chat session id
var sessionId = ChatSessionHelper.GetChatSessionId();
if (string.IsNullOrEmpty(sessionId)) throw new InvalidOperationException("No chat session id");
//now message will appear in all tabs
Clients.Group(sessionId).addNewMessageToPage(message);
}
Finally, the (simple) ChatSessionHelper class
public static class ChatSessionHelper
{
public static void SetChatSessionCookie()
{
var context = HttpContext.Current;
HttpCookie cookie = context.Request.Cookies.Get("Session") ?? new HttpCookie("Session", GenerateChatSessionId());
context.Response.Cookies.Add(cookie);
}
public static string GetChatSessionId()
{
return HttpContext.Current.Request.Cookies.Get("Session")?.Value;
}
public static string GenerateChatSessionId()
{
return Guid.NewGuid().ToString();
}
}

a solution widely adopted is to make the user register with his some kind of id with connection back , on the onConnected.
public override Task OnConnected()
{
Clients.Caller.Register();
return base.OnConnected();
}
and than the user returns with a call with some kind of your own id logic
from the clients Register Method
public void Register(Guid userId)
{
s_ConnectionCache.Add(userId, Guid.Parse(Context.ConnectionId));
}
and you keep the user ids in a static dictionary ( take care of the locks since you need it to be thread safe;
static readonly IConnectionCache s_ConnectionCache = new ConnectionsCache();
here
public class ConnectionsCache :IConnectionCache
{
private readonly Dictionary<Guid, UserConnections> m_UserConnections = new Dictionary<Guid, UserConnections>();
private readonly Dictionary<Guid,Guid> m_ConnectionsToUsersMapping = new Dictionary<Guid, Guid>();
readonly object m_UserLock = new object();
readonly object m_ConnectionLock = new object();
#region Public
public UserConnections this[Guid index]
=>
m_UserConnections.ContainsKey(index)
?m_UserConnections[index]:new UserConnections();
public void Add(Guid userId, Guid connectionId)
{
lock (m_UserLock)
{
if (m_UserConnections.ContainsKey(userId))
{
if (!m_UserConnections[userId].Contains(connectionId))
{
m_UserConnections[userId].Add(connectionId);
}
}
else
{
m_UserConnections.Add(userId, new UserConnections() {connectionId});
}
}
lock (m_ConnectionLock)
{
if (m_ConnectionsToUsersMapping.ContainsKey(connectionId))
{
m_ConnectionsToUsersMapping[connectionId] = userId;
}
else
{
m_ConnectionsToUsersMapping.Add(connectionId, userId);
}
}
}
public void Remove(Guid connectionId)
{
lock (m_ConnectionLock)
{
if (!m_ConnectionsToUsersMapping.ContainsKey(connectionId))
{
return;
}
var userId = m_ConnectionsToUsersMapping[connectionId];
m_ConnectionsToUsersMapping.Remove(connectionId);
m_UserConnections[userId].Remove(connectionId);
}
}
a sample call to Register form an android app
mChatHub.invoke("Register", PrefUtils.MY_USER_ID).get();
for JS it would be kind of the same
chat.client.register = function () {
chat.server.register(SOME_USER_ID);
}

Related

How can I get/create an instance of an object on Simple Injector container depending on the asp.net web api 2 session?

I have an asp.net web api 2 project in which I register a singleton LoginData
containerLocal.RegisterSingleton<ILoginData>(
() => new LoginData { LanguageCode = null, UserCode = null });
What I want to do is when a user log in to the app I register an instance of that Login data in my container
var loginData = AppContainer.Container.GetInstance<ILoginData>();
loginData.UserCode = userData.Username;
loginData.LanguageCode = "es";
and then, on whatever controller get that instance of user to perform any operation
my problem is that when I try to make a get request from 2 different browsers I get the last instance of Login data created in the app instead of the instance with the correct user
[Route("stock/menu")]
[HttpGet]
public async Task<IHttpActionResult> Menu()
{
var loginData = AppContainer.Container.GetInstance<ILoginData>();
return Ok(loginData);
}
example:
Login in chrome browser with username 'foo'
Make get request on InventarioMenu in chrome (gets LoginData with user 'foo')
Login in firefox browser with username 'bar'
Make get request on InventarioMenu in firefox (gets LoginData with user 'bar')
Make get request on InventarioMenu in chrome (gets LoginData with user 'bar')
There are multiple ways to solve this. A possible solution is to make an ILoginData implementation that stores information in a Session:
public sealed class SessionLoginData : ILoginData
{
public string UserCode
{
get { (string)this.Session[nameof(UserCode)]; }
set { this.Session[nameof(UserCode)] = value; }
}
public string LanguageCode
{
get { (string)this.Session[nameof(LanguageCode)]; }
set { this.Session[nameof(LanguageCode)] = value; }
}
private HttpSessionState Session => HttpContext.Current.Session;
}
Since this class itself contains no state (but delegates that to the HttpContext.Session), this implementation can be registered as singleton:
container.RegisterSingleton<ILoginData, SessionLoginData>();

Order process using SignalR asp.net core best practice

Below is the order process which i have implemented thorough signalR in asp.net core web api. Every thing is working fine except one scenario(Problem scenario given below) i need the best possible solution.
System Overview:
1) Customer Place new order (Client wait until order being processed by admin client).
2) The order is saved to the DB with ‘status=unknown ‘.
3) Admin is notified through hub about new order. (on a dashboard)
4) Admin accepts or decline new order then Order status is updated in database.
5) Customer is notified about the order, if is accepted or declined through SignalR
Problem scenario
The business rule that we have to implement is that the order should be automatically declined after 2 minutes if the Admin does not respond. In this case the server should automatically decline the order and the customer should be notified.
Solution 1: We thought of adding a timer on the Customer and Admin side, but we prefer the Timer to be somewhere on the server so we don't have to implement the timers on the customer and admin side.
Base Hub Controller
public abstract class ApiHubController<T> : Controller
where T : Hub
{
private readonly IHubContext _hub;
public IHubConnectionContext<dynamic> Clients { get; private set; }
public IGroupManager Groups { get; private set; }
protected ApiHubController(IConnectionManager signalRConnectionManager)
{
var _hub = signalRConnectionManager.GetHubContext<T>();
Clients = _hub.Clients;
Groups = _hub.Groups;
}
}
public class BaseHubController : ApiHubController<Broadcaster>
{
public BaseHubController(IConnectionManager signalRConnectionManager) : base(signalRConnectionManager)
{
}
}
Server side code (Place Order)
public class OrderController : BaseHubController
{
public async Task SendNotification([FromBody]NotificationDTO notify)
{
await Clients.Group(notify.AdminId.ToString()).SendNotificationToDashboard(notify); //notifing to admin for about //new order
}
public async Task NotifyDashboard(NotificationDTO model)
{
var sendNotification = SendNotification(model);//sending notification to admin dashboard
}
[HttpPost]
[Route("PlaceOrder")]
public IActionResult PlaceOrder([FromBody]OrderDTO order)//Coustomer place order
{
if (!ModelState.IsValid)
{
return new BadRequestObjectResult(ModelState);
}
var orderCode = _orderProvider.PlaceOrder(order, ValidationContainer);//save new order in database
var notify = order.GetNotificationModel();
notify.OrderId = orderCode;
NotifyDashboard(notify);
//Other code
return new OkObjectResult(new { OrderCode = orderCodeString, OrderId = orderCode });
}
}

IoC container implementation in controller

I want to use the IoC container in a method to check a logged in users company code when they submit a payment. I have two certificates in my settings class and an IF else statement to differentiate between each one.
public static string FDGCreditCardUserID
{
get
{
if (BillingController.currentcompanycode == 5)
return ConfigurationManager.AppSettings["5FDGCreditCardUserID"];
else
return ConfigurationManager.AppSettings["6FDGCreditCardUserID"];
}
}
public static string FDGCreditCardPassword
{
get
{
if (BillingController.currentcompanycode == 5)
return ConfigurationManager.AppSettings["5FDGCreditCardPassword"];
else
return ConfigurationManager.AppSettings["6FDGCreditCardPassword"];
}
}
Then in my IoC container
x.For<IFDGService>().Use<FDGService>().SetProperty(s =>
{
s.Url = Settings.FDGURL;
s.UserID = Settings.FDGCreditCardUserID;
s.Password = Settings.FDGCreditCardPassword;
s.Certificate = Settings.FDGCreditCardCertFilePath;
});
I have an FDGService that checks credentials but does not return to the IoC on payment submit to check the company code and apply the correct certificate.
SubmitPayment Method where the creditcard control contains the correct company code when i run it.
How do i get my application to select the correct certificate based on the updated company code. Seeing as users can have different company codes based on policies selected for payment. One company code at the moment can either be 5 or 6.
public ActionResult SubmitPayment([ConvertJSON]List<PayModel> payments)
{
List<TransactionModel> transactions = new List<TransactionModel>();
foreach (var pymt in payments)
{
var policyNumber = pymt.PolicyNumber.Trim();
TransactionModel trans = new TransactionModel() { Payment = pymt };
if (pymt.Selected)
{
var creditCardControl = UpdateCreditCardControl(policyNumber);
If you are using StructureMap it uses "Greedy Initialization", meaning when the constructor is called it will call the constructor with the most amount of arguments or parameters passed in.
private IFDGService service;
public MyController(IFDGService service)
{
this.service = service;
}
Then service will be available after IoC.Configure() is called.
Call IoC.Configure() whereever the application is started. google "where does Mvc start" or something like that.
to change the company code set it somewhere other than an instance variable in the controller, like a static class, I know static is bad, get it working and then make it better, since that would be complex to modify, and then get; set; when you need to.
I have to go to meeting, kinda rushed, hope that helps

Anonymous Private Chat in SignalR

I'm somewhat new to SignalR. I understand hubs to a limited degree, but I don't understand how two users can share a connection while excluding others.
My scenario is that I want an unauthenticated public website user to be able to initiate a private (not necessarily secure) chat session with a customer service user.
Is there an example or resource that my point me in the right direction?
I've looked at a few resources, including http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections but haven't found the right scenario.
You can create groups, so add some methods to your hub (a subscribe method should return a Task as they are asynchronous...)
public Task SubscribeToGroup(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
Then you publish notifications to users of that group as normal but via the groups collection...
public void BroadcastMessageToGroup(string groupName, string message)
{
Clients.Group(groupName).onCaptionReceived(message);
}
Now only subscribers of that particular group will get the message!
Hope this helps.
You can find a tutorial here for SignalR Groups.
http://www.asp.net/signalr/overview/signalr-20/hubs-api/working-with-groups
You can create a group in Hub's API, in this method each user is a member of that group. And they send a message to that group ( via the server), and because they are only 2 members they are the only one's who see the messages ( privatly)
You can also message a group member directly by connection ID. This requires your app to keep track of connection IDs of users as they connect and disconnect, but this isn't too difficult:
//Users: stores connection ID and user name
public static ConcurrentDictionary Users = new ConcurrentDictionary();
public override System.Threading.Tasks.Task OnConnected()
{
//Add user to Users; user will supply their name later. Also give them the list of users already connected
Users.TryAdd(Context.ConnectionId, "New User");
SendUserList();
return base.OnConnected();
}
//Send everyone the list of users (don't send the userids to the clients)
public void SendUserList()
{
Clients.All.UpdateUserList(Users.Values);
}
//Clients will call this when their user name is known. The server will then update all the other clients
public void GiveUserName(string name)
{
Users.AddOrUpdate(Context.ConnectionId, name, (key, oldvalue) => name);
SendUserList();
}
//Let people know when you leave (not necessarily immediate if they just close the browser)
public override System.Threading.Tasks.Task OnDisconnected()
{
string user;
Users.TryRemove(Context.ConnectionId, out user);
SendUserList();
return base.OnDisconnected();
}
//Ok, now we can finally send to one user by username
public void SendToUser(string from, string to, string message)
{
//Send to every match in the dictionary, so users with multiple connections and the same name receive the message in all browsers
foreach(KeyValuePair user in Users)
{
if (user.Value.Equals(to))
{
Clients.Client(user.Key).sendMessage(from, message);
}
}
}

Logging server events in signalR

I'm writing a C#-based web application using SignalR. So far I have a 'lobby' area (where open communication is allowed), and an 'session' area (where groups of 5 people can engage in private conversation, and any server interactions are only shown to the group).
What I'd like to do is create a 'logging' object in memory - one for each session (so if there are three groups of five people, I'd have three logging objects).
The 'session' area inherits from Hubs (and IDisconnect), and has several methods (Join, Send, Disconnect, etc.). The methods pass data back to the JavaScript client, which calls client-side JS functions. I've tried using a constructor method:
public class Session : Hub, IDisconnect
{
public class Logger
{
public List<Tuple<string, string, DateTime>> Log;
public List<Tuple<string, string, DateTime>> AddEvent(string evt, string msg, DateTime time)
{
if (Log == null)
{
Log = new List<Tuple<string, string, DateTime>>();
}
Log.Add(new Tuple<string, string, DateTime>(evt, msg, time));
return Log;
}
}
public Logger eventLog = new Logger();
public Session()
{
eventLog = new Logger();
eventLog.AddEvent("LOGGER INITIALIZED", "Logging started", DateTime.Now);
}
public Task Join(string group)
{
eventLog.AddEvent("CONNECT", "User connect", DateTime.Now);
return Groups.Add(Context.ConnectionId, group);
}
public Task Send(string group, string message)
{
eventLog.AddEvent("CHAT", "Message Sent", DateTime.Now);
return Clients[group].addMessage(message);
}
public Task Interact(string group, string payload)
{
// deserialise the data
// pass the data to the worker
// broadcast the interaction to everyone in the group
eventLog.AddEvent("INTERACTION", "User interacted", DateTime.Now);
return Clients[group].interactionMade(payload);
}
public Task Disconnect()
{
// grab someone from the lobby?
eventLog.AddEvent("DISCONNECT","User disconnect",DateTime.Now);
return Clients.leave(Context.ConnectionId);
}
}
But this results in the Logger being recreated every time a user interacts with the server.
Does anyone know how I'd be able to create one Logger per new session, and add elements to it? Or is there a simpler way to do this and I'm just overthinking the problem?
Hubs are created and disposed of all the time! Never ever put data in them that you expect to last (unless it's static).
I'd recommend creating your logger object as it's own class (not extending Hub/IDisconnect).
Once you have that create a static ConcurrentDictionary on the hub which maps SignalR groups (use these to represent your sessions) to loggers.
When you have a "Join" method triggered on your hub it's easy as looking up the group that the connection was in => Sending the logging data to the groups logger.
Checkout https://github.com/davidfowl/JabbR when it comes to making "rooms" and other sorts of groupings via SignalR
Hope this helps!

Categories

Resources