Tuesday, February 27, 2018

Understanding SAP Cloud Platform Destination in SAP UI5 app

SAP UI5 is a client javascript library the browser downloads from web server (front end server) to render web UI. Usually the business data come from other backend servers, which is in a different domain from the web server. Due to the browser's same origin policy, browser cannot make cross domain requests to the backend server.

For SAP UI5 app deployed in SAP Cloud Platform (SCP), the same origin issue can be solved by with Destination. Basically Destination in SCP works as a proxy based on url path. When defining Destination in SCP, a unique name and url is configured in SCP admin. So SCP will know where to send the odate request to the backend server.

Another benefit to use Destination is, the SCP admin can change the Destination url after the web app is already deployed to web server, so admin can switch the odata backend url from production server to testing server without updating the web application.

In SAP UI5 app, when a odata data source is added into the project, neo-app.json will create an item with path and target information as below:
    {
      "path": "/John/NorthwindDest",
      "target": {
        "type": "destination",
        "name": "NorthwindDest"
      },
      "description": "Northwind odata service"
    }

The target.name must match the SCP Destination name, so SCP knows which destination is used by the app. The path is an url pattern, which means when an odata request is sent to server, the server will check, if the url match the path defined in neo-app.json, then redirect the request to matched Destination.

The Path of "/John/NorthwindDest" is just a unique identify and its value can be anything you define, for example, you can replace "/John/NorthwindDestto "/Server/MyUniqueID". And then in the UI5 application, for any xmlhttprequests sent from client with the path starting with "/Server/MyUniqueID", the server will redirect the request to the matched Destination's url .

The UI5 app manifest.json data source and model definition should use the same pattern in their uri definition.
"dataSources": {
"Northwind.svc": {
"uri": "/John/NorthwindDest/v2/Northwind/Northwind.svc/",
"type": "OData",
"settings": {
"odataVersion": "2.0",
"localUri": "localService/metadata.xml"
}
}
}

"models": {
"": {
"uri": "/John/NorthwindDest/v2/Northwind/Northwind.svc/",
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultBindingMode": "OneWay",
"defaultCountMode": "Request"
},
"dataSource": "Northwind.svc",
"preload": true
}
},


This can be verified using javascript network trace, when UI5 app starts it will request oData metadata from backend, the request sent to server has the below url
https://webidetesting7287641-i826633trial.dispatcher.hanatrial.ondemand.com/John/NorthwindDest/v2/Northwind/Northwind.svc/$metadata?sap-langu

And SAP Cloud Platform will switch the url based on the Destination match to
http://services.odata.org/V2/Northwind/Northwind.svc/$metadata?sap-language=EN

The destination is defined as


In order to run a SAP UI5 app on different web server, like asp.net, then the request handler must implement the logic to handle Destination proxy, so it needs parsing the content in neo-app.json, and then when client requests come, it needs to check whether the url pattern match a destination, if so, the request needs to be redirect to the url defined in the destination.

NSURLSessionDelegate and derived class

NSURLSessionDelegate
     URLSession:didBecomeInvalidWithError:
     URLSession:didReceiveChallenge:completionHandler:
     URLSessionDidFinishEventsForBackgroundURLSession:

    NSURLSessionTaskDelegate
        URLSession:task:didReceiveChallenge:completionHandler:
        URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
        URLSession:task:needNewBodyStream:
        URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
        URLSession:task:didFinishCollectingMetrics:
                NSURLSessionTaskMetrics
        URLSession:task:didCompleteWithError:
        URLSession:task:willBeginDelayedRequest:completionHandler:
        URLSession:taskIsWaitingForConnectivity:

         NSURLSessionDataDelegate
            URLSession:dataTask:didReceiveResponse:completionHandler:
            URLSession:dataTask:didBecomeDownloadTask:
            URLSession:dataTask:didBecomeStreamTask:
            URLSession:dataTask:didReceiveData:
            URLSession:dataTask:willCacheResponse:completionHandler:

         NSURLSessionDownloadDelegate
            URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
            URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
            URLSession:downloadTask:didFinishDownloadingToURL:

         NSURLSessionStreamDelegate
            URLSession:readClosedForStreamTask:
            URLSession:writeClosedForStreamTask:
            URLSession:betterRouteDiscoveredForStreamTask:
            URLSession:streamTask:didBecomeInputStream:outputStream:





Wednesday, February 21, 2018

CORS issue when loading html from local file system in ios WKWebView

On iOS, UIWebView does not have any limitation for cross domain javascript requests (except for the this issue), so html file loaded from local host or a domain, can make xmlhttprequest to another remote domain and get response properly.

However, iOS WKWebView limits the cross domain js request, so when xmlhttpRequests are sent to a different domain, even if the server returns expected, the WKWebView will block the content and instead returning an error to xmlhttprequest.

To allow cross domain requests work on WKWebView, the server which handles the xmlhttpRequest issued from a different domain must be configured to tell the browser to allow the xmlhttpRequest to accept the response, which is how CORS specification tries to handle.

Note if the xmlhttprequest is sent from local html file, for uiwebview, the origin header in request is not set. For wkwebView, the origin header in request is "null";


For testing purpose, asp.net core is used to create the server web app, the client side html file is loaded from local using a cordova ios project.

1. In the asp.net core webapi project, adding the below line in startup.cs Configure method

app.UseCors(builder =>
    builder.WithOrigins("*"));

Then sending a cross domain xmlhttprequest from a local html file to remote server will work. Checking Fiddler trace, it will show in the server response, a new CORS header is added

Access-Control-Allow-Origin: *

This header tells browser that server trust the request to be sent from any other domain, and browser should pass the response to the xmlhttprequest object.


2. After the basic request works, now we try to make the request with credential and cookie information

First updating the client js script to set xmlhttprequest withCredentials to true
            xhr.open('GET', url, true);
            xhr.withCredentials = true;
            xhr.onreadystatechange = handler;

            xhr.send();

Now sending the same request as in section 1 will fail. The Fiddler trace shows the client request does not changed anything with the new withCredentials=true parameter, and the server's response also does not have any change. So it means the withCredentials flag is only applied to browser itself, and not involved in the http server request and response.

Actually when this error happens, the browser console logs an error of
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.
Failed to load resource: Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.

That indicates the exact problem. withCredential does not work when origin is set to "*" on server side.

The problem is if the html file is loaded from local file system using "file://" scheme, then the wkwebview will set the request origin as null, and on server side CORS does not to set allowed origin to "null". That means in order to make cross domain requests work for local html/js files loaded in wkwebview, the request must have a normal origin with scheme of http:// or https.

One option to work around this issue is using an inapp web server to server the local content as http://localhost:8080, iconic framework uses this approach as described at
https://ionicframework.com/docs/wkwebview/
https://blog.ionicframework.com/wkwebview-for-all-a-new-webview-for-ionic/
https://docs.google.com/document/d/19VQ-n7hGr9IDPPstQqU8_8WgqUh7R6sgQfL2neoT-Xw/edit

Another option is using unpublished api to use nsurlprotocol with wkwebview as mentioned at
https://github.com/yeatse/NSURLProtocol-WebKitSupport

Neither of them is ideal, hopefully the future ios release can allow CORS for html files loaded from local file system.

3. Since html file loaded from local cannot pass the CORS check on browser side, so the next testing is loading the html file from another http domain. So a regular origin can be set for Access-Control-Allow-Origin header.
For this purpose, another asp.net core app has been created to host the html page at the same host but a different port.
Now repeating the same testing in xhr request to set withCrdential = true
and the request header are
Origin: http://yyzn00596231a.amer.global.corp.sap:60208
Host: yyzn00596231a.amer.global.corp.sap:63132
and the response header has the expected header of
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208

However, the client still cannot received the response, the javascript console logs the error of
"Failed to load resource: Credentials flag is true, but access-control-allow-credential is not 'true'"

So in order to support credential, the server needs to specify access-control-allow-credential header in the response, for asp.net core project, this can be done with the below code
      public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(builder =>
                builder.WithOrigins("http://yyzn00596231a.amer.global.corp.sap:60208")
                .AllowCredentials());

            app.UseMvc();
        }

Repeat the testing again and the xhr request can get the response as expected. The response header are
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208

4. The next testing is enable basic authentication in the xhr request with credential enabled. To do so, add https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic into the asp.net core project as a dependent project, and then update the below source code to enable basic auth
   public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddMvc();
            services.AddAuthentication()
              .AddBasicAuthentication(credentials =>
                    Task.FromResult(
                        credentials.username == "user"
                        && credentials.password == "password"));

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseCors(builder =>
                builder.WithOrigins("http://yyzn00596231a.amer.global.corp.sap:60208")
                .AllowCredentials());
            app.UseMvc();
            app.UseAuthentication();
        }



 [Route("api/[controller]")]
    [Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)]    public class ValuesController : Controller{

...
}

First testing on ipad mobile safari browser, when server side returns the below response
HTTP/1.1 401 Unauthorized
Server: Kestrel
WWW-Authenticate: Basic realm="MySite"
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://yyzn00596231a.amer.global.corp.sap:60208
X-SourceFiles: =?UTF-8?B?QzpcVGVtcFxjb3JzXGNvcnNcYXBpXHZhbHVlcw==?=
X-Powered-By: ASP.NET
Date: Fri, 23 Feb 2018 21:52:43 GMT
Content-Length: 0


The safari browser does not pop up a dialog to let user input user name and password, instead it just return the response to xhr response handler.

Repeating the same testing with cordova wkwebview and it gets the same result, the 401 response was directly returned to xml response handler with readystate of 4.

In addition, if directly loading the basic auth url
http://http://yyzn00596231a.amer.global.corp.sap:63132/api/values 
to Mobile safari or Cordova wkwebview, then it gets the challenge callback at didReceiveAuthenticationChallenge for wkwebwview, and also shows the 401 challenge dialog box for mobile safari, so it means the browser and wkwebview just do not allow the challenge to be handled by user for CORS requests.












 


Saturday, February 10, 2018

Fix pod update issue after change the admin user of the mac

After changing the own of the mac, there may be errors when updating pod in the existing xcode project. The main reason is the brew, gem, ruby, cocoapod fail to upgrade due the permission issue.

The below are steps to update the mac

1.  give permission to install homebrew
sudo chmod -R 777 /usr/local/Homebrew

2. Install homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

3. Install ruby
brew install ruby

4. update gem
gem update --system

5. install cocoapods
sudo gem install cocoapods -n/usr/local/bin

6. go to your xcode project to update pod dependent libraries
pod update 

The above steps also applies to the refresh mac pod initialization.