If you have any ideas that might improve this, I wouldn't mind making this better. This code comes from a need to debug a PHP REST API server from a Xamarin.Forms client, sometimes through Charles web debugging proxy. Feel free to tell me I'm doing something wrong, because, if it seems like an anti-pattern, I probably just don't know any better yet. In any case, I was thinking about just putting together an example solution, but I'm short on time. If I get time to round it out enough, I might try to make a blog post for this stuff or something:
/App.cs
namespace MobileShell
{
public class App : Application
{
/// <summary>
/// Gets or sets the ServerType. This is used to setup the debugging environment
/// </summary>
/// <value>The type of the server.</value>
public static APIServerType ServerType { get; set; }
/// <summary>
/// Gets or sets the API service.
/// </summary>
/// <value>The API service.</value>
public static ApiService ApiService { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="MobileShell.App"/> class.
/// </summary>
public App ()
{
CrossConnectivity.Current.ConnectivityChanged += OnConnectivityChanged;
/* Howto Debug with REST API:
* Cookie for XDebug is set by AuthenticatedHttpClientHandler for local debugging (tested on a MacBook Pro OS X 10.10.3 homebrew with nginx, php-fpm, xdebug via a phalcon microframework codebase).
* PHPStorm debugging doesn't seem to work with Android, at least when running through a proxy. (Needs testing w/refactored code.)
* Proxy assumes 127.0.0.1:8888 for iOS and 10.0.3.2 for Android.
* If you are using something like the Charles web debugging proxy, this should work well.
* Set IsProxy = true and update the ApiUrl/ApiDomain below to your local API dev server IP/hostname.
*/
//Settings.IsDebug = Debugger.IsAttached;
ServerType = APIServerType.OfficeLocal;
Settings.IsDebug = true;
Settings.IsProxy = true;
string proxy = "";
Device.OnPlatform (
Android: () => {
if (ServerType == APIServerType.OfficeLocal) {
proxy = "10.0.1.10";
} else {
proxy = "192.168.0.45";
}
},
Default: () => {
proxy = "127.0.0.1";
}
);
string apiUrl;
string apiDomain;
switch (ServerType) {
case APIServerType.SOLocal:
apiUrl = "http://192.168.0.45/api/v1";
apiDomain = "192.168.0.45";
break;
case APIServerType.OfficeLocal:
apiUrl = "http://10.0.1.10/api/v1";
apiDomain = "10.0.1.10";
break;
case APIServerType.AndroidLocal:
apiUrl = "http://10.0.3.2/api/v1";
apiDomain = "10.0.3.2";
break;
case APIServerType.iOSLocal:
apiUrl = "http://127.0.0.1/api/v1";
apiDomain = "127.0.0.1";
break;
case APIServerType.Remote:
apiUrl = "http://example.co/api/v1";
apiDomain = "example.co";
break;
default:
apiUrl = "http://example.co/api/v1";
apiDomain = "example.co";
break;
}
Settings.ApiUrl = apiUrl;
Settings.ApiDomain = apiDomain;
Settings.ProxyUrl = string.Format ("http://{0}:8888", proxy);
App.ApiService = new ApiService(Settings.ApiUrl);
// Check for cached credentials
InitUser ();
MainPage = GetMainPage ();
}
/// <summary>
/// Initializes the <see cref="MobileShell.App"/> class.
/// </summary>
static App ()
{
//https://github.com/paulcbetts/Fusillade#how-do-i-use-this-with-modernhttpclient
Locator.CurrentMutable.RegisterConstant (new NativeMessageHandler (), typeof(HttpMessageHandler));
//Locator.CurrentMutable.RegisterConstant (new AuthenticatedHttpClientHandler (Settings.ApiUrl), typeof(HttpMessageHandler));
}
/// <summary>
/// Raises the connectivity changed event.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">E.</param>
public void OnConnectivityChanged (object sender, Connectivity.Plugin.Abstractions.ConnectivityChangedEventArgs e)
{
Debug.WriteLine (string.Format ("Connectivity Changed - IsConnected: {0}", e.IsConnected));
//Application.Current.MainPage.DisplayAlert("Connectivity Changed", "IsConnected: " + args.IsConnected.ToString(), "OK");
}
/// <summary>
/// Gets the main navigation page.
/// </summary>
/// <returns>The main navigation page.</returns>
public Page GetMainPage ()
{
Page currentPage;
if (IsAuthenticated == true) {
currentPage = new MainMenuPage ();
} else {
currentPage = new LoginPage ();
}
//NavigationPage mainNavigationPage = new LifetimeNavigationPage (currentPage);
NavigationPage mainNavigationPage = new NavigationPage (currentPage);
return mainNavigationPage;
}
/// <summary>
/// Initializes the application User.
/// </summary>
public void InitUser()
{
UsersService usersService = new UsersService(App.ApiService);
App.UserViewModel = new UserViewModel (usersService);
if (Settings.UserId != Guid.Empty) {
User authUser = App.UserViewModel.GetUser (Settings.UserId);
if (authUser != null) {
App.UserViewModel.User = authUser;
App.ApiService.SetUser (authUser);
Debug.WriteLine (string.Format("User: {0}", App.UserViewModel.User.UserName));
} /*else {
await App.UserViewModel.GetRemoteUser (Settings.UserId);
}*/
}
}
/// <summary>
/// Determines if the specified host is reachable.
/// </summary>
/// <returns><c>true</c> if is host reachable the specified host; otherwise, <c>false</c>.</returns>
/// <param name="host">Host.</param>
public static Task<bool> IsHostReachable (string host)
{
return Task.Run (async () => {
bool isConnected = CrossConnectivity.Current.IsConnected;
if (isConnected == true) {
bool isHostReachable;
if (ServerType == APIServerType.Remote) {
isHostReachable = await CrossConnectivity.Current.IsRemoteReachable (host).ConfigureAwait (false);
} else {
isHostReachable = await CrossConnectivity.Current.IsReachable (host).ConfigureAwait (false);
}
return isHostReachable;
} else {
return false;
}
});
}
protected override void OnStart ()
{
// Handle when your app starts
}
protected override void OnSleep ()
{
// Handle when your app sleeps
}
protected override void OnResume ()
{
// Handle when your app resumes
}
}
}
/Services/ApiService.cs:
using System;
using System.Net.Http;
using Fusillade;
using Refit;
using MobileShell.Helpers;
using MobileShell.Models;
namespace MobileShell.Services
{
public class ApiService : IApiService
{
/// <summary>
/// The API base address.
/// </summary>
public const string ApiBaseAddress = "http://example.co/api/v1";
//public const string ApiBaseAddress = "http://example.dev/api/v1";
/// <summary>
/// Gets or sets the API base address.
/// </summary>
/// <value>The API base address.</value>
Uri _apiBaseAddress { get; set; }
/// <summary>
/// Gets or sets the create client func.
/// </summary>
/// <value>The create client.</value>
Func<HttpMessageHandler, IMobileShellApi> _createClient { get; set; }
/// <summary>
/// Gets or sets the AuthenticatedHttpClientHandler.
/// </summary>
/// <value>The http client handler.</value>
AuthenticatedHttpClientHandler _httpClientHandler { get; set; }
/// <summary>
/// Creates the HttpClient.
/// </summary>
/// <returns>The Refit REST client.</returns>
/// <param name="messageHandler">Message handler.</param>
IMobileShellApi CreateClient (HttpMessageHandler messageHandler)
{
HttpClient client = new HttpClient (messageHandler) {
BaseAddress = _apiBaseAddress,
Timeout = TimeSpan.FromSeconds(60)
};
return RestService.For<IMobileShellApi> (client);
}
/// <summary>
/// Initializes a new instance of the <see cref="MobileShell.Services.ApiService"/> class.
/// </summary>
/// <param name="apiBaseAddress">API base address.</param>
public ApiService (string apiBaseAddress = null)
{
_apiBaseAddress = new Uri(apiBaseAddress ?? ApiBaseAddress);
Func<HttpMessageHandler, IMobileShellApi> createClient = CreateClient;
_httpClientHandler = new AuthenticatedHttpClientHandler (_apiBaseAddress.OriginalString);
_background = new Lazy<IMobileShellApi>(() => createClient(
new RateLimitedHttpMessageHandler(_httpClientHandler, Priority.Background)));
_userInitiated = new Lazy<IMobileShellApi>(() => createClient(
new RateLimitedHttpMessageHandler(_httpClientHandler, Priority.UserInitiated)));
_speculative = new Lazy<IMobileShellApi>(() => createClient(
new RateLimitedHttpMessageHandler(_httpClientHandler, Priority.Speculative)));
}
/// <summary>
/// Sets the user used for AuthenticatedHttpClientHandler requests that have the [Headers("Authorization: MobileShell")] attribute set.
/// </summary>
/// <param name="user">User.</param>
public void SetUser (User user)
{
_httpClientHandler.SetUser (user);
}
/// <summary>
/// The Background RateLimitedHttpMessageHandler.
/// </summary>
readonly Lazy<IMobileShellApi> _background;
/// <summary>
/// The UserInitiated RateLimitedHttpMessageHandler.
/// </summary>
readonly Lazy<IMobileShellApi> _userInitiated;
/// <summary>
/// The speculative RateLimitedHttpMessageHandler.
/// </summary>
readonly Lazy<IMobileShellApi> _speculative;
/// <summary>
/// Gets the Background RateLimitedHttpMessageHandler.
/// </summary>
/// <value>The background.</value>
public IMobileShellApi Background
{
get {
return _background.Value;
}
}
/// <summary>
/// Gets the UserInitiated RateLimitedHttpMessageHandler.
/// </summary>
/// <value>The user initiated.</value>
public IMobileShellApi UserInitiated
{
get {
return _userInitiated.Value;
}
}
/// <summary>
/// Gets the Speculative RateLimitedHttpMessageHandler.
/// </summary>
/// <value>The speculative.</value>
public IMobileShellApi Speculative
{
get {
return _speculative.Value;
}
}
}
}
/IDefaultProxyFactory.cs:
using System.Net;
namespace MobileShell
{
public interface IDefaultProxyFactory
{
IWebProxy GetDefaultProxy(string address, bool bypassOnLocal);
string GetHostNameIP (string hostName);
}
}
Droid Project
/DefaultProxyFactory_Droid.cs:
using System;
using System.Net;
using MobileShell.Droid;
using Xamarin.Forms;
[assembly: Dependency(typeof(DefaultProxyFactory_Droid))]
namespace MobileShell.Droid
{
public class DefaultProxyFactory_Droid : IDefaultProxyFactory
{
public string GetHostNameIP (string hostName)
{
IPAddress[] hostInfo = Dns.GetHostAddresses (hostName);
if (hostInfo.Length > 0) {
return hostInfo [0].ToString ();
} else {
return string.Empty;
}
}
public IWebProxy GetDefaultProxy (string address, bool bypassOnLocal)
{
return new WebProxy (address, bypassOnLocal);
}
}
}
#
iOS Project
/DefaultProxyFactory_iOS.cs:
using System;
using System.Net;
using MobileShell.iOS;
using Xamarin.Forms;
[assembly: Dependency(typeof(DefaultProxyFactory_iOS))]
namespace MobileShell.iOS
{
public class DefaultProxyFactory_iOS : IDefaultProxyFactory
{
public string GetHostNameIP (string hostName)
{
IPAddress[] hostInfo = Dns.GetHostAddresses (hostName);
if (hostInfo.Length > 0) {
return hostInfo [0].ToString ();
} else {
return string.Empty;
}
}
public IWebProxy GetDefaultProxy (string address, bool bypassOnLocal)
{
return new WebProxy (address, bypassOnLocal);
}
}
}
#
/Services/IApiService.cs:
using System.Collections.Generic;
namespace MobileShell.Services
{
public interface IApiService
{
IMobileShellApi Speculative { get; }
IMobileShellApi UserInitiated { get; }
IMobileShellApi Background { get; }
}
}
/Services/IMobileShellApi.cs:
using System.Collections.Generic;
using Refit;
using MobileShell.Dtos;
using MobileShell.Models;
using System;
using System.Net.Http;
namespace MobileShell.Services
{
/// <summary>
/// https://github.com/paulcbetts/refit
/// </summary>
//[Headers("Accept: application/json")]
public interface IMobileShellApi
{
[Post("/users/addUser")]
IObservable<HttpResponseMessage> CreateUser([Body(BodySerializationMethod.Json)] UserDto newUser);
[Post("/users/auth")]
IObservable<HttpResponseMessage> LoginUser([Body(BodySerializationMethod.Json)] AuthenticationDto auth);
[Get("/users/{userId}")]
[Headers("Authorization: MobileShell")]
IObservable<HttpResponseMessage> GetUser([Body(BodySerializationMethod.Json)] Guid userId);
//Task<string> GetUser([Body(BodySerializationMethod.Json)] Guid userId);
}
}
/Helpers/AuthenticatedHttpClientHandler.cs:
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MobileShell.Helpers;
using MobileShell.Models;
using ModernHttpClient;
using Xamarin.Forms;
namespace MobileShell.Helpers
{
/// <summary>
/// Authenticated http client handler.
/// https://github.com/paulcbetts/refit#authorization-dynamic-headers-redux
/// </summary>
public class AuthenticatedHttpClientHandler : NativeMessageHandler
{
/// <summary>
/// The base address.
/// </summary>
readonly string baseAddress;
/// <summary>
/// The user used for authorized requests.
/// </summary>
User _user;
/// <summary>
/// The authorization headers.
/// </summary>
AuthorizationHeaders _authorizationHeaders;
/// <summary>
/// The is initialized.
/// </summary>
//public static bool IsInitialized = false;
/// <summary>
/// Uses the native WebProxy if true.
/// </summary>
public static bool IsProxy = false;
/// <summary>
/// Initializes a new instance of the <see cref="MobileShell.Helpers.AuthenticatedHttpClientHandler"/> class.
/// </summary>
/// <param name="apiBaseAddress">API base address.</param>
public AuthenticatedHttpClientHandler(string apiBaseAddress)
{
baseAddress = apiBaseAddress;
if (Settings.IsProxy == true) {
var defaultProxy = DependencyService.Get<IDefaultProxyFactory> ();
if (defaultProxy != null) {
IsProxy = true;
UseProxy = true;
Proxy = defaultProxy.GetDefaultProxy (Settings.ProxyUrl, true);
}
}
/*if (Debugger.IsAttached) {
var defaultProxy = DependencyService.Get<IDefaultProxyFactory> ();
if (defaultProxy != null) {
UseProxy = true;
Proxy = defaultProxy.GetDefaultProxy ();
}
}*/
}
/// <summary>
/// Sets the authenticated user.
/// </summary>
/// <param name="user">User.</param>
public void SetUser (User user)
{
_user = user;
}
/// <summary>
/// Sets the authorization headers.
/// </summary>
/// <param name="data">Data.</param>
void setAuthorizationHeaders (string data)
{
if (_authorizationHeaders == null) {
if (_user != null &&
!string.IsNullOrEmpty (_user.PublicId) &&
!string.IsNullOrEmpty (_user.PrivateKey)) {
_authorizationHeaders = new AuthorizationHeaders {
PublicId = _user.PublicId,
PrivateKey = _user.PrivateKey
};
} else {
throw new Exception ("***AuthenticatedHttpClientHandler*** Request authentication data missing (_user)");
}
}
_authorizationHeaders.Timestamp = DateTime.UtcNow.ToUnixTime ().ToString ();
string message = _authorizationHeaders.PrivateKey + _authorizationHeaders.Timestamp + data;
_authorizationHeaders.Hash = Crypto.HashHMAC(message, _authorizationHeaders.PrivateKey);
}
/// <summary>
/// Sends the request async.
/// </summary>
/// <returns>The async.</returns>
/// <param name="request">Request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
/*if (IsInitialized == false) {
bool isProxyAvailable;
if (Device.OS == TargetPlatform.Android) {
//Android localhost is 10.0.3.2 for all intents and purposes afaik
isProxyAvailable = await Connectivity.Plugin.CrossConnectivity.Current.IsRemoteReachable (host: "10.0.3.2", port: 8888, msTimeout: 5000);
} else {
isProxyAvailable = await Connectivity.Plugin.CrossConnectivity.Current.IsRemoteReachable (host: "127.0.0.1", port: 8888, msTimeout: 5000);
}
if (isProxyAvailable == true) {
var defaultProxy = DependencyService.Get<IDefaultProxyFactory> ();
if (defaultProxy != null) {
IsProxy = true;
UseProxy = true;
Proxy = defaultProxy.GetDefaultProxy ();
}
}
IsInitialized = true;
}*/
var authorizationHeader = request.Headers.Authorization;
if (authorizationHeader != null)
{
if (_user != null && !string.IsNullOrEmpty (_user.PublicId) && !string.IsNullOrEmpty (_user.PrivateKey)) {
string requestBody;
if (request.Method == HttpMethod.Get) {
requestBody = "";
} else {
requestBody = await request.Content.ReadAsStringAsync ().ConfigureAwait(false);
}
setAuthorizationHeaders (requestBody);
/*
* TODO: Refactor authorization headers to use something like an AuthenticationHeaderValue class, like
* the example in the github link above.
*/
//request.Headers.Authorization = null;
//request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
Debug.WriteLine (string.Format ("X-Message: {0}", _authorizationHeaders.Hash));
Debug.WriteLine (string.Format ("X-Public: {0}", _authorizationHeaders.PublicId));
Debug.WriteLine (string.Format ("X-Timestamp: {0}", _authorizationHeaders.Timestamp));
request.Headers.Add ("X-Message", _authorizationHeaders.Hash);
request.Headers.Add ("X-Public", _authorizationHeaders.PublicId);
request.Headers.Add ("X-Timestamp", _authorizationHeaders.Timestamp);
} else {
/*
* Maybe it'd be better to redirect to login here. However, if your IApiService method requires the
* authorization header--you should already be authenticated.
*/
throw new Exception ("***AuthenticatedHtttpClientHandler*** Request authentication data missing (User PublicId/PrivateKey)");
}
}
//This sets up the debugging session for XDebug
if (this.CookieContainer.Count == 0) {
setXDebugCookie ();
} else {
bool isDebugSet = false;
foreach (Cookie cookie in this.CookieContainer.GetCookies(new Uri(baseAddress))) {
if (cookie.Value == "PHPSTORM") {
isDebugSet = true;
break;
}
}
if (isDebugSet == false) {
setXDebugCookie ();
}
}
/*if (Settings.IsDebug == true && Settings.IsProxy == true) {
Debug.WriteLine ("***AuthenticatedHtttpClientHandler*** Note: Proxy running.");
}*/
Debug.WriteLine (string.Format("***AuthenticatedHtttpClientHandler*** Request: {0}", request.RequestUri.OriginalString));
return await base.SendAsync(request, cancellationToken);
}
void setXDebugCookie ()
{
CookieContainer cookieContainer = new CookieContainer ();
cookieContainer.Add (new Uri (baseAddress), new Cookie ("XDEBUG_SESSION", "PHPSTORM"));
this.CookieContainer = cookieContainer;
}
}
}