How to ignore invalid ssl certificate in Nativescript


#1

How to ignore invalid ssl certificate in Nativescript.

I have searched the internet but didn’t found any exhaustive answer. I am able to do it for iOS by making changes in the info.plist file but for Android I am stucked.


#2

The default Android HTTP client uses its own SSLSocketFactory, so you cannot set it the usual way via HttpsURLConnection. You have to use your own http client that supports the customization of its socket factory.

I used okhttp, so you must add it to the gradle dependencies in app\App_Resources\Android\app.gradle:

dependencies {
  compile 'com.squareup.okhttp3:okhttp:3.10.0'
//	compile 'com.android.support:recyclerview-v7:+'
}

Here is a declaration file for okhttp3, that you need to reference in references.d.ts:

declare module okhttp3 {
  class OkHttpClient {
		newCall(req: Request): Call
	}

	module OkHttpClient {
		class Builder {
			sslSocketFactory(sslSocketFactory: javax.net.ssl.SSLSocketFactory, trustManager: javax.net.ssl.X509TrustManager): Builder
			hostnameVerifier(hostnameVerifier: javax.net.ssl.HostnameVerifier): Builder
			addInterceptor(interceptor: IInterceptor)
			build(): OkHttpClient
		}
	}

  class Request {
		url(): HttpUrl
	}

	class Response {
		body(): ResponseBody
		code(): number
		message(): string
		close(): void
		request(): Request
	}

	class ResponseBody {
		string(): string
	}

	class Call {
		enqueue(responseCallback: Callback): void
	}

	interface ICallback {
		onResponse(call: Call, response: Response): void
		onFailure(call: Call, e: java.io.IOException): void
	}
	class Callback implements ICallback {
		constructor(implementation: ICallback)

		onResponse(call: Call, response: Response): void
		onFailure(call: Call, e: java.io.IOException): void
	}

	module Request {
		class Builder {
			url(url: string): Builder
			header(name: string, value: string): Builder
			post(body: RequestBody) : Builder
			put(body: RequestBody) : Builder
			delete() : Builder
			build(): Request
		}
	}

	class RequestBody {
		static create(contentType: MediaType, content: string): RequestBody
	}

	class MediaType {
		static parse(string: string): MediaType
	}

	class HttpUrl {
		static parse(url: string): HttpUrl
		newBuilder(): HttpUrl.Builder
		toString(): string
	}
	module HttpUrl {
		class Builder {
			addQueryParameter(name: string, value: string): Builder
			build(): HttpUrl
		}
	}
}

And here is a wrapper class for HTTP that uses okhttp for Android and the standard client for ios. Note that I’ve just began working with nativescript/typescript/angular, so the code may contain ugly things, but it works (I didn’t test the error handling though). It’s also a proof of concept so it must be strengthened for production:

import { Injectable, NgZone } from "@angular/core";
import * as app from "tns-core-modules/application";
import { HttpClient, HttpParams, HttpHeaders, HttpResponse } from "@angular/common/http";
import { AppConfig } from "./app-config";
import { Observable } from "rxjs/Observable";
import { map, catchError } from "rxjs/operators";
import { Subject } from "rxjs/Subject";

@Injectable()
export class HttpWrapperService {
	sslSocketFactory?: javax.net.ssl.SSLSocketFactory;

	private okhttp: okhttp3.OkHttpClient;
	private jsonMediaType: okhttp3.MediaType;

	constructor(private http: HttpClient, private zone: NgZone, appConfig: AppConfig) {
		if (app.android && appConfig.enableSelfSignedCertificate) {
			// create a SSL factory that supports self-signed certificates
			const trustAllCerts = new javax.net.ssl.X509TrustManager({
				getAcceptedIssuers: (): java.security.cert.X509Certificate[] => Array.create(java.security.cert.X509Certificate, 0),
				checkClientTrusted: (arg0: java.security.cert.X509Certificate[], args1: string) => { },
				checkServerTrusted: (arg0: java.security.cert.X509Certificate[], args1: string) => { }
			});
			const certs = [trustAllCerts];
			const sc = javax.net.ssl.SSLContext.getInstance("TLS");
			sc.init(null!, certs, new java.security.SecureRandom());
			this.sslSocketFactory = sc.getSocketFactory();

			const hostnameVerifier = new javax.net.ssl.HostnameVerifier({
				verify: (hostname: string, session: javax.net.ssl.SSLSession): boolean => true
			});

			// build http client once
			this.okhttp = new okhttp3.OkHttpClient.Builder()
				.sslSocketFactory(this.sslSocketFactory, trustAllCerts)
				.hostnameVerifier(hostnameVerifier)
				.build();

			this.jsonMediaType = okhttp3.MediaType.parse("application/json; charset=utf-8");
		}
	}

	/**
	 * Construct a GET request which interprets the body as JSON and returns it.
	 *
	 * @return an `Observable` of the body as type `T`.
	 */
	get<T>(url: string, options?: {
		headers?: { [name: string]: string; };
		params?: { [param: string]: string; };
		responseType?: "json";
	}): Observable<T> {
		if (this.okhttp) {
			return this.okhttpRequest("get", url, options);

		} else {
			return this.http.get<T>(url, options).pipe(
				catchError(err => {
					let error: HttpWrapperErrorResponse;
					if (err.error instanceof ErrorEvent) {
						error = new HttpWrapperErrorResponse(0, err.error.message);
					} else {
						error = new HttpWrapperErrorResponse(err.status, err.error);
					}

					return Observable.throw(error);
				})
			);
		}
	}

	private okhttpRequest<T>(method: "get" | "post" | "put" | "delete", url: string, options?: {
		body?: string;
		headers?: { [name: string]: string; };
		params?: { [param: string]: string; };
		responseType?: "json";
	}): Observable<T> {
		// build the request
		const builder = new okhttp3.Request.Builder();

		// method
		switch (method) {
			case "post":
				builder.post(this.createOkhttpRequestBody(options && options.body));
			case "put":
				builder.put(this.createOkhttpRequestBody(options && options.body));
				break;
			case "delete":
				builder.delete();
				break;
		}

		// headers
		if (options && options.headers) {
			Object.keys(options.headers).forEach(key => {
				builder.header(key, options.headers![key]);
			});
		}

		// URL and parameters
		if (options && options.params) {
			const urlBuilder = okhttp3.HttpUrl.parse(url).newBuilder();
			Object.keys(options.params).forEach(key => {
				urlBuilder.addQueryParameter(key, options.params![key]);
			});
			builder.url(urlBuilder.build().toString());

		} else {
			builder.url(url);
		}

		const request = builder.build();

		// make the call asynchronously and feed the response observable
		const response: Subject<T> = new Subject(); // TODO manage unsubscribe to cancel the call

		this.okhttp.newCall(request).enqueue(new okhttp3.Callback({
			onResponse: (call: okhttp3.Call, resp: okhttp3.Response): void => {
				const body = resp.body().string();
				console.log(resp.code());
				console.log(body);
				resp.close();

				this.zone.run(() => { // to allow UI change detection by angular
					// if the response is OK, return the parsed body and complete
					// TODO manage other kinds of response types
					if (HttpWrapperService.isResponseOk(resp.code())) {
						response.next(JSON.parse(body));
						response.complete();

					// otherwise return an error
					} else {
						response.error(new HttpWrapperErrorResponse(resp.code(), resp.message()));
					}
				});
			},

			onFailure: (call: okhttp3.Call, e: java.io.IOException): void => {
				this.zone.run(() => {
					response.error(new HttpWrapperErrorResponse(0, e.getMessage()));
				});
			}
		}));

		return response;
	}

	private createOkhttpRequestBody(body?: string): okhttp3.RequestBody {
		if (body) {
			return okhttp3.RequestBody.create(this.jsonMediaType, body);
		} else {
			return okhttp3.RequestBody.create(this.jsonMediaType, "");
		}
	}

	static isResponseOk(status: number): boolean {
		return status >= 200 && status < 300;
	}
}

/**
 * An error response.
 * If `status` is 0 then it is a system error, otherwise it is a HTTP error.
 */
export class HttpWrapperErrorResponse {
	constructor(readonly status: number, readonly message: string) {}
}

How to enable SSL certificate pinning in nativescript angular app?
#3

If you also want to use WebSockets you can hack nativescript-websockets and use the same factory as created for okhttp.
In node_modules\nativescript-websockets\websockets.android.js:

  • in the constructor var NativeWebSockets = function(url, options) {, add at the end this._sslSocketFactory = options.sslSocketFactory;.
  • In NativeWebSockets.prototype._reCreate = function() {, replace the end with:
 if (isWSS) {
 	var socketFactory;
 	if (this._sslSocketFactory) {
 		socketFactory = this._sslSocketFactory;
 	} else {
 		//noinspection JSUnresolvedFunction,JSUnresolvedVariable
 		var sslContext = javax.net.ssl.SSLContext.getInstance( "TLS" );
 		sslContext.init( null, null, null );
 		//noinspection JSUnresolvedFunction
 		socketFactory = sslContext.getSocketFactory();
 		//noinspection JSUnresolvedFunction
 	}
 	
 	this._socket.setSocket( socketFactory.createSocket() );
 }

You can now pass the factory as an option:
const ws = new WS(url, { headers: this.wsHeaders, sslSocketFactory: this.http.sslSocketFactory });

I created an issue on github, so the owner may implement something like that in a next version.
And I suppose you could redefine the above methods in your own code to avoid hacking the library code.