using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using Serilog; using TINK.Model.Bikes.BikeInfoNS.BluetoothLock; using TINK.Model.Device; using TINK.Model.Logging; using TINK.Repository.Exception; using TINK.Repository.Request; using TINK.Repository.Response; namespace TINK.Repository { /// Object which manages calls to copri. public class CopriCallsHttps : ICopriServer { /// Builds requests. private IRequestBuilder requestBuilder; /// Initializes a instance of the copri calls https object. /// Host to connect to. /// Provides app related info (app name and version, merchantid) to pass to COPRI. /// Two letter ISO language name. /// Session cookie if user is logged in, null otherwise. public CopriCallsHttps( Uri copriHost, AppContextInfo appContextInfo, string uiIsoLangugageName, string sessionCookie = null) { m_oCopriHost = copriHost ?? throw new System.Exception($"Can not construct {GetType()}- object. Uri of copri host must not be null."); UserAgent = appContextInfo != null ? appContextInfo.UserAgent : throw new System.Exception($"Can not construct {GetType()}- object. User agent must not be null or empty."); requestBuilder = string.IsNullOrEmpty(sessionCookie) ? new RequestBuilder(appContextInfo.MerchantId, uiIsoLangugageName) as IRequestBuilder : new RequestBuilderLoggedIn(appContextInfo.MerchantId, uiIsoLangugageName, sessionCookie); } /// Holds the URL for rest calls. private Uri m_oCopriHost; /// Spacifies name and version of app. private string UserAgent { get; } /// Returns true because value requested form copri server are returned. public bool IsConnected => true; /// Gets the merchant id. public string MerchantId => requestBuilder.MerchantId; /// Gets the session cookie if user is logged in, an empty string otherwise. public string SessionCookie => requestBuilder.SessionCookie; /// Logs user in. /// Mailaddress of user to log in. /// Password to log in. /// Id specifying user and hardware. /// Response which holds auth cookie public async Task DoAuthorizationAsync( string mailAddress, string password, string deviceId) => await DoAuthorizationAsync( m_oCopriHost.AbsoluteUri, requestBuilder.DoAuthorization(mailAddress, password, deviceId), () => requestBuilder.DoAuthorization(mailAddress, "********", deviceId), UserAgent); /// Logs user out. /// Response which holds auth cookie public async Task DoAuthoutAsync() => await DoAuthoutAsync(m_oCopriHost.AbsoluteUri, requestBuilder.DoAuthout(), UserAgent); /// Gets bikes available. /// Response holding list of bikes. public async Task GetBikesAvailableAsync() => await GetBikesAvailableAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesAvailable(), UserAgent); /// Gets a list of bikes reserved/ booked by acctive user. /// Response holding list of bikes. public async Task GetBikesOccupiedAsync() { try { return await GetBikesOccupiedAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesOccupied(), UserAgent); } catch (NotSupportedException) { // No user logged in. await Task.CompletedTask; return ResponseHelper.GetBikesOccupiedNone(); } } /// Get list of stations. /// List of files. public async Task GetStationsAsync() => await GetStationsAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetStations(), UserAgent); /// Get authentication keys. /// Id of the bike to get keys for. /// Response holding authentication keys. public async Task GetAuthKeys(string bikeId) => await GetAuthKeysAsync(m_oCopriHost.AbsoluteUri, requestBuilder.CalculateAuthParameters(bikeId), UserAgent); /// Gets booking request response. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Booking response. public async Task DoReserveAsync( string bikeId, Uri operatorUri) => await DoReserveAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.DoReserve(bikeId), UserAgent); /// Gets canel booking request response. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on cancel booking request. public async Task DoCancelReservationAsync( string bikeId, Uri operatorUri) => await DoCancelReservationAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.DoCancelReservation(bikeId), UserAgent); /// Get authentication keys. /// Id of the bike to get keys for. /// Holds the uri of the operator or null, in case of single operator setup. /// Response holding authentication keys. public async Task CalculateAuthKeysAsync( string bikeId, Uri operatorUri) => await GetAuthKeysAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.CalculateAuthParameters(bikeId), UserAgent); /// Notifies COPRI about start of returning sequence. /// Operator specific call. /// Id of the bike to return.+ /// Holds the uri of the operator or null, in case of single operator setup. /// Response on notification about start of returning sequence. public async Task StartReturningBike( string bikeId, Uri operatorUri) => await DoStartReturningBike( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.StartReturningBike(bikeId), UserAgent); /// Updates lock state for a booked bike. /// Id of the bike to update locking state for. /// Geolocation of lock. /// New locking state. /// Holds the filling level percentage of the battery. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on updating locking state. public async Task UpdateLockingStateAsync( string bikeId, lock_state state, Uri operatorUri, LocationDto location, double batteryLevel, IVersionInfo versionInfo) => await DoUpdateLockingStateAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.UpateLockingState(bikeId, state, location, batteryLevel, versionInfo), UserAgent); /// Gets booking request request. /// Id of the bike to book. /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. /// Holds the filling level percentage of the battery. /// Holds the uri of the operator or null, in case of single operator setup. /// Requst on booking request. public async Task DoBookAsync( string bikeId, Guid guid, double batteryPercentage, Uri operatorUri) => await DoBookAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.DoBook(bikeId, guid, batteryPercentage), UserAgent); /// Books a bike and starts opening bike. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on booking request. public async Task BookAvailableAndStartOpeningAsync( string bikeId, Uri operatorUri) => await DoBookAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.BookAvailableAndStartOpening(bikeId), UserAgent); /// Books a bike and starts opening bike. /// Id of the bike to book. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on booking request. public async Task BookReservedAndStartOpeningAsync( string bikeId, Uri operatorUri) => await DoBookAsync( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.BookReservedAndStartOpening(bikeId), UserAgent); /// Returns a bike. /// Id of the bike to return. /// Geolocation of lock. /// Provides info about hard and software. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on returning request. public async Task DoReturn( string bikeId, LocationDto location, ISmartDevice smartDevice, Uri operatorUri) => await DoReturn( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.DoReturn(bikeId, location, smartDevice), UserAgent); /// Returns a bike and starts closing. /// Id of the bike to return. /// Provides info about hard and software. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on returning request. public async Task ReturnAndStartClosingAsync( string bikeId, ISmartDevice smartDevice, Uri operatorUri) => await DoReturn( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.ReturnAndStartClosing(bikeId, smartDevice), UserAgent); /// Submits feedback to copri server. /// Id of the bike to which the feedback is related to. /// Null if bike has no engine or charge is unknown. Otherwise the charge filling level of the drive battery. /// True if bike is broken. /// General purpose message or error description. /// Holds the uri of the operator or null, in case of single operator setup. /// Response on submitting feedback request. public async Task DoSubmitFeedback( string bikeId, int? currentChargeBars, string message, bool isBikeBroken, Uri operatorUri) => await DoSubmitFeedback( operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, requestBuilder.DoSubmitFeedback(bikeId, currentChargeBars, message, isBikeBroken), UserAgent); /// Submits mini survey to copri server. /// Collection of answers. public Task DoSubmitMiniSurvey(IDictionary answers) => DoSubmitMiniSurvey( m_oCopriHost.AbsoluteUri, requestBuilder.DoSubmitMiniSurvey(answers), UserAgent); /// Logs user in. /// Host to connect to. /// Command to log user in. /// Response which holds auth cookie public static async Task DoAuthorizationAsync( string copriHost, string command, Func displayCommand, string userAgent = null) { #if !WINDOWS_UWP /// Extract session cookie from response. string response = string.Empty; try { response = await PostAsync( copriHost, command, userAgent, displayCommand); // Do not include password into exception output when an error occurres. } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException); } throw; } return CopriCallsStatic.DeserializeResponse(response, (version) => new UnsupportedCopriVersionDetectedException()); #else return null; #endif } /// Logs user out. /// Host to connect to. /// Command to log user out. public static async Task DoAuthoutAsync( string p_strCopriHost, string p_oCommand, string userAgent = null) { #if !WINDOWS_UWP string l_oLogoutResponse; try { l_oLogoutResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException); } throw; } /// Extract session cookie from response. return CopriCallsStatic.DeserializeResponse(l_oLogoutResponse, (version) => new UnsupportedCopriVersionDetectedException()); #else return null; #endif } /// /// Get list of stations from file. /// /// URL of the copri host to connect to. /// Command to get stations. /// List of files. public static async Task GetStationsAsync( string p_strCopriHost, string p_oCommand, string userAgent = null) { #if !WINDOWS_UWP string response; try { response = await PostAsync(p_strCopriHost, p_oCommand, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } throw; } // Extract bikes from response. return CopriCallsStatic.DeserializeResponse( response, (version) => CopriCallsMonkeyStore.GetEmptyStationsAllResponse(version)); #else return null; #endif } /// Gets a list of bikes from Copri. /// URL of the copri host to connect to. /// Command to get bikes. /// Response holding list of bikes. public static async Task GetBikesAvailableAsync( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string response; try { response = await PostAsync(copriHost, command, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } throw; } // Extract bikes from response. return CopriCallsStatic.DeserializeResponse( response, (version) => CopriCallsMonkeyStore.GetEmptyBikesAvailableResponse(version)); #else return null; #endif } /// Gets a list of bikes reserved/ booked by acctive user from Copri. /// URL of the copri host to connect to. /// Command to post. /// Response holding list of bikes. public static async Task GetBikesOccupiedAsync( string p_strCopriHost, string p_oCommand, string userAgent = null) { #if !WINDOWS_UWP string response; try { response = await PostAsync(p_strCopriHost, p_oCommand, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); } throw; } // Extract bikes from response. return CopriCallsStatic.DeserializeResponse( response, (version) => CopriCallsMonkeyStore.GetEmptyBikesReservedOccupiedResponse(version)); #else return null; #endif } /// Get auth keys from COPRI. /// Host to connect to. /// Command to log user in. /// Response on booking request. public static async Task GetAuthKeysAsync( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string bikesAvaialbeResponse; try { bikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception exception) { if (exception.GetIsConnectFailureException()) { throw new WebConnectFailureException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", exception); } if (exception.GetIsForbiddenException()) { throw new WebForbiddenException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", exception); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(bikesAvaialbeResponse)?.shareejson; #else return null; #endif } /// Gets booking request response. /// Host to connect to. /// Command to log user in. /// Response on booking request. public static async Task DoReserveAsync( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string bikesAvaialbeResponse; try { bikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception exception) { if (exception.GetIsConnectFailureException()) { throw new WebConnectFailureException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception); } if (exception.GetIsForbiddenException()) { throw new WebForbiddenException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(bikesAvaialbeResponse)?.shareejson; #else return null; #endif } /// Gets canel booking request response. /// Host to connect to. /// Command to log user in. /// Response on cancel booking request. public static async Task DoCancelReservationAsync( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string l_oBikesAvaialbeResponse; try { l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(l_oBikesAvaialbeResponse)?.shareejson; #else return null; #endif } public static async Task DoStartReturningBike( string copriHost, string command, string agent = null) { #if !WINDOWS_UWP string response; try { response = await PostAsync(copriHost, command, agent); } catch (System.Exception exception) { if (exception.GetIsConnectFailureException()) { throw new WebConnectFailureException("Benachrichtigung von Start der Rückgabe wegen Netzwerkfehler fehlgeschlagen.", exception); } if (exception.GetIsForbiddenException()) { throw new WebForbiddenException("Benachrichtigung von Start der Rückgabe wegen Netzwerkfehler fehlgeschlagen.", exception); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(response)?.shareejson; #else return null; #endif } public static async Task DoUpdateLockingStateAsync( string copriHost, string command, string agent = null) { #if !WINDOWS_UWP string bikesAvaialbeResponse; try { bikesAvaialbeResponse = await PostAsync(copriHost, command, agent); } catch (System.Exception exception) { if (exception.GetIsConnectFailureException()) { throw new WebConnectFailureException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", exception); } if (exception.GetIsForbiddenException()) { throw new WebForbiddenException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", exception); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(bikesAvaialbeResponse)?.shareejson; #else return null; #endif } public static async Task DoBookAsync( string copriHost, string command, string agent = null) { #if !WINDOWS_UWP string bikesAvaialbeResponse; try { bikesAvaialbeResponse = await PostAsync(copriHost, command, agent); } catch (System.Exception exception) { if (exception.GetIsConnectFailureException()) { throw new WebConnectFailureException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception); } if (exception.GetIsForbiddenException()) { throw new WebForbiddenException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", exception); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(bikesAvaialbeResponse)?.shareejson; #else return null; #endif } public static async Task DoReturn( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string doReturnResponse; try { doReturnResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(doReturnResponse)?.shareejson; #else return null; #endif } public async Task DoSubmitFeedback( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string userFeedbackResponse; try { userFeedbackResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(userFeedbackResponse)?.shareejson; #else return null; #endif } /// Submits mini survey to copri server. public async Task DoSubmitMiniSurvey( string copriHost, string command, string userAgent = null) { #if !WINDOWS_UWP string miniSurveyResponse; try { miniSurveyResponse = await PostAsync(copriHost, command, userAgent); } catch (System.Exception l_oException) { if (l_oException.GetIsConnectFailureException()) { throw new WebConnectFailureException("Senden der Miniumfrage aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } if (l_oException.GetIsForbiddenException()) { throw new WebForbiddenException("Senden der der Miniumfrage aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); } throw; } // Extract bikes from response. return JsonConvertRethrow.DeserializeObject>(miniSurveyResponse)?.shareejson; #else return null; #endif } /// http get- request. /// Ulr to get info from. /// response from server public static async Task Get(string Url) { string result = string.Empty; HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(Url); myRequest.Method = "GET"; using (var myResponse = await myRequest.GetResponseAsync()) { using (var sr = new StreamReader(myResponse.GetResponseStream(), Encoding.UTF8)) { result = sr.ReadToEnd(); } } return result; } /// http- post request. /// Command to send. /// Command to display/ log used for error handling. /// Address of server to communicate with. /// Response as text. /// An unused member PostAsyncHttpClient using HttpClient for posting was removed 2020-04-02. private static async Task PostAsync( string uRL, string command, string userAgent = null, Func displayCommand = null) { if (string.IsNullOrEmpty(command)) { Log.ForContext().Fatal("Can not post command. Command must not be null or empty."); throw new ArgumentException("Can not post command. Command must not be null or empty."); } if (string.IsNullOrEmpty(uRL)) { Log.ForContext().Fatal("Can not post command. Host must not be null or empty."); throw new ArgumentException("Can not post command. Host must not be null or empty."); } // Get display version of command to used for display/ logging (password should never be included in output) Func displayCommandFunc = displayCommand ?? delegate () { return command; }; try { #if !WINDOWS_UWP var l_strHost = uRL; // Returns a http request. var request = WebRequest.CreateHttp(l_strHost); request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; request.UserAgent = userAgent; // Workaround for issue https://bugzilla.xamarin.com/show_bug.cgi?id=57705 // If not KeepAlive is set to true Stream.Write leads arbitrarily to an object disposed exception. request.KeepAlive = true; byte[] postData = Encoding.UTF8.GetBytes(command); request.ContentLength = postData.Length; // Get the request stream. using (Stream dataStream = await request.GetRequestStreamAsync()) { // Write the data to the request stream. await dataStream.WriteAsync(postData, 0, postData.Length); } // Get the response. var webResponse = await request.GetResponseAsync() as HttpWebResponse; if (webResponse == null) { throw new System.Exception(string.Format("Reserve request failed. Response form from server was not of expected type.")); } if (webResponse.StatusCode != HttpStatusCode.OK) { throw new CommunicationException(string.Format( "Posting request {0} failed. Expected status code is {1} but was {2}.", displayCommandFunc(), HttpStatusCode.OK, webResponse.StatusCode)); } string response = string.Empty; // Get the request stream. using (Stream l_oDataStream = webResponse.GetResponseStream()) using (StreamReader l_oReader = new StreamReader(l_oDataStream)) { // Read the content. response = l_oReader.ReadToEnd(); // Display the content. Console.WriteLine(response); // Clean up the streams. webResponse.Close(); } Log.ForContext().Verbose("Post command {DisplayCommand} to host {URL} received {ResponseText:j}.", displayCommandFunc(), uRL, response); return response; #else return null; #endif } catch (System.Exception exception) { Log.ForContext().InformationOrError("Posting command {DisplayCommand} to host {URL} failed. {Exception}.", displayCommandFunc(), uRL, exception); throw; } } } }