private async Task ConnectWebSocket(SignInSession session, string seed, string rememberMe) { HttpResponseMessage response; try { response = await _ws.Connect(session, seed, rememberMe); } catch (Exception ex) { if (ex.Message == "Web Socket already connected") { throw new UserAlreadySignedIn(session.Username); } throw; } if (response.IsSuccessStatusCode) { return; } var exception = ParseGenericErrors(await response.Content.ReadAsStringAsync(), response.StatusCode); if (exception != null) { throw exception; } throw new Exception($"Unknown error during SignIn: {response.StatusCode}"); }
public async Task <HttpResponseMessage> Connect(SignInSession session, string seedString, string rememberMe, int reconnectDelay = 0) { if (_connected) { throw new WebSocketError(WsAlreadyConnected, session.Username); } var timeout = false; var timeoutToOpenWebSocket = SetTimeout( () => { if (!_connected && !Reconnecting) { timeout = true; throw new WebSocketError("timeout", ""); } }, 10000 ); var url = $"{WsUtils.GetWsUrl(_config.Endpoint)}api?appId={_config.AppId}&sessionId={session.SessionId}&clientId={_clientId}"; // TODO: handle timeouts var webSocket = new WebSocket(url); webSocket.Opened += (sender, e) => OnOpened(sender, e, timeout, timeoutToOpenWebSocket); webSocket.DataReceived += OnDataReceived; webSocket.MessageReceived += async(sender, args) => await OnMessageReceived(sender, args, webSocket, seedString, session); webSocket.Closed += async(sender, args) => await OnClosed(sender, args, session, seedString, rememberMe, reconnectDelay, timeout); webSocket.Error += OnError; var result = await webSocket.OpenAsync(); return(result ? new HttpResponseMessage(HttpStatusCode.OK) : new HttpResponseMessage(HttpStatusCode.InternalServerError)); }
private async Task OnClosed(object sender, EventArgs args, SignInSession session, string seedString, string rememberMe, int reconnectDelay, bool timeout) { await _logger.Log("CLOSED"); if (timeout) { return; } if (args is ClosedEventArgs e) { // TODO var serviceRestart = e.Code == 1012; // statusCodes['Service Restart'] var clientDisconnected = e.Code == 3000; //statusCodes['No Pong Received'] var attemptToReconnect = serviceRestart || clientDisconnected; // || !e.wasClean // closed without explicit call to ws.close() if (attemptToReconnect) { var delay = serviceRestart && reconnectDelay <= 0 ? 0 : (reconnectDelay > 0 ? reconnectDelay + BackoffRetryDelay : 1000); Reconnecting = true; await Reconnect(session, seedString, rememberMe, !_reconnected && State != null?State : null, delay); } else if (e.Code == 3001 /*statusCodes['Client Already Connected']*/) { // TODO: reject websocketError? throw new WebSocketError(WsAlreadyConnected, session.Username); } else { Init(null, null, null, null, null, null); } } }
public async Task <SignUpResult> SignUp(SignUpRequest signUpRequest) { try { ValidateSignUpOrSignInInput(signUpRequest.Username, signUpRequest.Password); if (signUpRequest.Profile != null) { ValidateProfile(signUpRequest.Profile); } if (string.IsNullOrEmpty(signUpRequest.Email)) { throw new Errors.EmailNotValid(); } if (!Config.RememberMeOptions.ContainsKey(signUpRequest.RememberMe)) { throw new RememberMeValueNotValid(); } var username = signUpRequest.Username.ToLower(); var email = signUpRequest.Email.ToLower(); var appId = _config.AppId; var seed = Utils.GenerateSeed(); var(sessionId, creationDate, userId) = await GenerateKeysAndSignUp(username, signUpRequest.Password, seed, email, signUpRequest.Profile); var session = new SignInSession { Username = username, SessionId = sessionId, CreationDate = creationDate.ToString(CultureInfo.InvariantCulture) }; var seedString = Convert.ToBase64String(seed); _localData.SaveSeedString(signUpRequest.RememberMe, appId, username, seedString); _localData.SignInSession(signUpRequest.RememberMe, username, sessionId, creationDate.ToString(CultureInfo.InvariantCulture)); await ConnectWebSocket(session, seedString, signUpRequest.RememberMe); return(new SignUpResult { Username = username, UserId = userId, Email = email, Profile = signUpRequest.Profile, }); } catch (Exception ex) { if (!(ex is IError e)) { throw new UnknownServiceUnavailable(ex); } switch (e.Name) { case "ParamsMustBeObject": case "UsernameMissing": case "UsernameAlreadyExists": case "UsernameCannotBeBlank": case "UsernameMustBeString": case "UsernameTooLong": case "PasswordMissing": case "PasswordCannotBeBlank": case "PasswordTooShort": case "PasswordTooLong": case "PasswordMustBeString": case "EmailNotValid": case "ProfileMustBeObject": case "ProfileCannotBeEmpty": case "ProfileHasTooManyKeys": case "ProfileKeyMustBeString": case "ProfileKeyTooLong": case "ProfileValueMustBeString": case "ProfileValueCannotBeBlank": case "ProfileValueTooLong": case "RememberMeValueNotValid": case "TrialExceededLimit": case "AppIdNotSet": case "AppIdNotValid": case "UserAlreadySignedIn": case "ServiceUnavailable": throw; default: throw new UnknownServiceUnavailable(ex); } } }
public void Init(Action resolveConnection = null, Action <IError> rejectConnection = null, SignInSession session = null, string seedString = null, string rememberMe = null, UserState state = null) { if (_pingTimeout > 0) { ClearTimeout(_pingTimeout); } // TODO: find out the scope of this code //for (const property of Object.keys(this)) { // delete this[property]; //} if (Instance4Net != null) { Instance4Net.Dispose(); Instance4Net = null; } _connected = false; _resolveConnection = resolveConnection; _rejectConnection = rejectConnection; _connectionResolved = false; Session.Username = session != null && !string.IsNullOrEmpty(session.Username) ? session.Username : ""; Session.SessionId = session != null && !string.IsNullOrEmpty(session.SessionId) ? session.SessionId : ""; Session.CreationDate = session != null && !string.IsNullOrEmpty(session.CreationDate) ? session.CreationDate : ""; _seedString = seedString; Keys.Clear(); _rememberMe = rememberMe; _pendingRequests.Clear(); State = state ?? new UserState { Databases = new Dictionary <string, Database>(), DbIdToHash = new Dictionary <string, string>(), DbNameToHash = new Dictionary <string, string>(), }; }
private async Task OnMessageReceived(object sender, MessageReceivedEventArgs e, WebSocket webSocket, string seedString, SignInSession session) { //_logger.Log(sender.ToString()); _ = _logger.Log($"RECEIVED - {e.Message}"); try { var msg = JObject.Parse(e.Message); var route = msg["route"]?.ToString().ToLower() ?? ""; switch (route) { case "connection": // TODO: pass parameters Init(null, null, session, seedString, null, null); Instance4Net = webSocket; //this.heartbeat() _connected = true; var connectionMessage = JsonConvert.DeserializeObject <ConnectionMessage>(e.Message); Keys.Salts = connectionMessage.KeySalts; _encryptedValidationMessage = connectionMessage.EncryptedValidationMessage.Data; await SetKeys(seedString); break; case "ping": //this.heartbeat() _ = _logger.Log("SENT - PONG"); const string action = "Pong"; Instance4Net.Send(JsonConvert.SerializeObject(new { action })); break; case "applytransactions": // TODO: in progress var dbId = msg["dbId"].ToString(); var dbNameHash = msg["dbNameHash"].ToString(); // || State.dbIdToHash[dbId] if (!State.Databases.TryGetValue(dbNameHash, out var database)) { throw new Exception("Missing database"); } // queue guarantees transactions will be applied in the order they are received from the server if (database.ApplyTransactionsQueue.Count == 0) { // take a spot in the queue and proceed applying so the next caller knows queue is not empty database.ApplyTransactionsQueue.Enqueue(null); } else { // wait until prior batch in queue finishes applying successfully var promise = new TaskCompletionSource <int>(); void Resolve() { var localPromise = promise; localPromise.SetResult(0); } database.ApplyTransactionsQueue.Enqueue(Resolve); await promise.Task; } var openingDatabase = msg["dbNameHash"] != null && msg["dbKey"] != null; if (openingDatabase) { var dbKeyString = AesGcmUtils.DecryptString(Keys.EncryptionKey, msg["dbKey"].ToString()); database.DbKeyString = dbKeyString; // TODO //database.dbKey = await AesGcmUtils.GetKeyFromKeyString(dbKeyString); } //if (!database.dbKey) throw new Error('Missing db key') if (msg["bundle"] != null) { /*const bundleSeqNo = message.bundleSeqNo * const base64Bundle = message.bundle * const compressedString = await crypto.aesGcm.decryptString(database.dbKey, base64Bundle) * const plaintextString = LZString.decompress(compressedString) * const bundle = JSON.parse(plaintextString) * * database.applyBundle(bundle, bundleSeqNo)*/ } /*const newTransactions = message.transactionLog * await database.applyTransactions(newTransactions)*/ if (!database.Init) { State.DbIdToHash[dbId] = dbNameHash; database.DbId = dbId; database.Init = true; _ = _logger.Log("DB - RECEIVED MESSAGE"); database.ReceivedMessage(); } if (msg["buildBundle"] != null) { BuildBundle(database); } // start applying next batch in queue when this one is finished applying successfully database.ApplyTransactionsQueue.Dequeue(); if (database.ApplyTransactionsQueue.Count > 0) { var startApplyingNextBatchInQueue = database.ApplyTransactionsQueue.Peek(); startApplyingNextBatchInQueue(); } break; case "signout": case "updateuser": case "deleteuser": case "createdatabase": case "getdatabase": case "opendatabase": case "insert": case "update": case "delete": case "batchtransaction": case "bundle": case "validatekey": case "getpasswordsalts": var requestId = msg["requestId"]?.ToString().ToLower() ?? ""; if (string.IsNullOrEmpty(requestId)) { // TODO: log warning Console.WriteLine("Missing request id"); return; } //var request = this.requests[requestId]; if (!_pendingRequests.TryGetValue(requestId, out var request)) { // TODO: log warning Console.WriteLine($"Request {requestId} no longer exists!"); return; } else if (request.Resolve == null || request.Reject == null) { return; } var response = msg["response"]?.ToString().ToLower() ?? ""; var status = msg["response"]["status"]?.ToString().ToLower() ?? ""; var statusCode = HttpStatusCode.BadRequest; var successfulResponse = !string.IsNullOrEmpty(response) && !string.IsNullOrEmpty(status) && Enum.TryParse(status, out statusCode) && statusCode == HttpStatusCode.OK; if (successfulResponse) { request.Resolve(requestId); } else { request.Reject(requestId, route, statusCode, response); } break; default: Console.WriteLine($"Received unknown message from backend: {msg}"); break; } } catch (Exception exception) { _ = _logger.Log($"ERROR - {exception.Message}"); } }
// TODO: handle state public async Task Reconnect(SignInSession session, string seed, string rememberMe, UserState state, int reconnectDelay = 0) { await Task.FromException(new NotImplementedException()); }