Quantcast
Channel: Xamarin.Forms — Xamarin Community Forums
Viewing all articles
Browse latest Browse all 58056

An iOS/Android Client/Server Debug Setup Example

$
0
0

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;
        }
    }
}

Viewing all articles
Browse latest Browse all 58056

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>