├── android
├── .settings
│ └── org.eclipse.buildship.core.prefs
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── localz
│ │ ├── pinch
│ │ ├── models
│ │ │ ├── HttpResponse.java
│ │ │ └── HttpRequest.java
│ │ └── utils
│ │ │ ├── JsonUtil.java
│ │ │ ├── KeyPinStoreUtil.java
│ │ │ └── HttpUtil.java
│ │ ├── PinchPackage.java
│ │ └── RNPinch.java
├── .project
├── build.gradle
├── gradlew.bat
└── gradlew
├── .gitignore
├── RNPinch.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── samitha.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── samitha.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── RNPinch
├── RNPinch.h
└── RNPinch.m
├── react-native-pinch-new.podspec
├── package.json
├── index.js
└── README.md
/android/.settings/org.eclipse.buildship.core.prefs:
--------------------------------------------------------------------------------
1 | connection.project.dir=
2 | eclipse.preferences.version=1
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | android/build/
2 | android/release/
3 | android/.idea
4 | android/.gradle
5 | android/local.properties
6 | *.iml
7 | .DS_Store
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samitha9125/react-native-pinch-new/HEAD/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/RNPinch.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RNPinch.xcodeproj/project.xcworkspace/xcuserdata/samitha.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/samitha9125/react-native-pinch-new/HEAD/RNPinch.xcodeproj/project.xcworkspace/xcuserdata/samitha.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Sep 27 11:09:28 IST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/RNPinch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RNPinch.xcodeproj/xcuserdata/samitha.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | RNPinch.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/RNPinch/RNPinch.h:
--------------------------------------------------------------------------------
1 | //
2 | // RNNativeFetch.h
3 | // medipass
4 | //
5 | // Created by Paul Wong on 13/10/16.
6 | // Copyright © 2016 Localz. All rights reserved.
7 | //
8 |
9 | #import
10 | #if __has_include()
11 | #import "React/RCTBridgeModule.h"
12 | #import "React/RCTLog.h"
13 | #else
14 | #import "RCTBridgeModule.h"
15 | #import "RCTLog.h"
16 | #endif
17 |
18 | @interface RNPinch : NSObject
19 |
20 | @end
21 |
--------------------------------------------------------------------------------
/android/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | android
4 | Project android created by Buildship.
5 |
6 |
7 |
8 |
9 | org.eclipse.buildship.core.gradleprojectbuilder
10 |
11 |
12 |
13 |
14 |
15 | org.eclipse.buildship.core.gradleprojectnature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/react-native-pinch-new.podspec:
--------------------------------------------------------------------------------
1 | require 'json'
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, './package.json')))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = package['name']
7 | s.version = package['version']
8 | s.summary = package['description']
9 | s.license = package['license']
10 |
11 | s.authors = package['author']
12 | s.homepage = package['homepage']
13 | s.platform = :ios, "9.0"
14 |
15 | s.source = { :git => "https://github.com/samitha9125/react-native-pinch-new.git", :tag => "master" }
16 | s.source_files = "RNPinch/*.{h,m}"
17 |
18 | s.dependency 'React'
19 | end
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/pinch/models/HttpResponse.java:
--------------------------------------------------------------------------------
1 | package com.localz.pinch.models;
2 |
3 | import com.facebook.react.bridge.WritableMap;
4 |
5 | public class HttpResponse {
6 | public int statusCode;
7 | public WritableMap headers;
8 | public String bodyString;
9 | public String statusText;
10 |
11 | public HttpResponse() {}
12 |
13 | public HttpResponse(int statusCode, WritableMap headers, String bodyString, String statusText) {
14 | this.statusCode = statusCode;
15 | this.headers = headers;
16 | this.bodyString = bodyString;
17 | this.statusText = statusText;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.library"
2 |
3 | //import com.android.build.OutputFile
4 |
5 | def safeExtGet(prop, fallback) {
6 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
7 | }
8 |
9 | android {
10 | compileSdkVersion safeExtGet('compileSdkVersion', 28)
11 | buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
12 |
13 | defaultConfig {
14 | minSdkVersion 16
15 | targetSdkVersion safeExtGet('targetSdkVersion', 28)
16 | versionCode 1
17 | versionName "1.0"
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation "com.facebook.react:react-native:+" // From node_modules
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-pinch-new",
3 | "version": "0.1.15",
4 | "description": "React Native fetch with SSL Pinning support",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/samitha9125/react-native-pinch-new"
12 | },
13 | "keywords": [
14 | "react-native",
15 | "ios",
16 | "android",
17 | "SSL",
18 | "SSL-Pinning",
19 | "Network"
20 | ],
21 | "author": "Samitha9125",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/samitha9125/react-native-pinch-new/issues"
25 | },
26 | "homepage": "https://github.com/samitha9125/react-native-pinch-new#readme",
27 | "devDependencies": {
28 | "react": "15.3.2",
29 | "react-native": "^0.34"
30 | },
31 | "peerDependencies": {
32 | "react-native": ">=0.34 <1.0.0"
33 | },
34 | "dependencies": {
35 | "q": "^1.4.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/pinch/models/HttpRequest.java:
--------------------------------------------------------------------------------
1 | package com.localz.pinch.models;
2 |
3 | import org.json.JSONObject;
4 |
5 | public class HttpRequest {
6 | public String endpoint;
7 | public String method;
8 | public JSONObject headers;
9 | public String body;
10 | public boolean ignore_ssl;
11 | public String[] certFilenames;
12 | public int timeout;
13 |
14 | private static final int DEFAULT_TIMEOUT = 10000;
15 |
16 | public HttpRequest() {
17 | this.timeout = DEFAULT_TIMEOUT;
18 | }
19 |
20 | public HttpRequest(String endpoint) {
21 | this.endpoint = endpoint;
22 | this.timeout = DEFAULT_TIMEOUT;
23 | }
24 |
25 | public HttpRequest(String endpoint, String method, JSONObject headers, String body, String[] certFilenames, int timeout) {
26 | this.endpoint = endpoint;
27 | this.method = method;
28 | this.headers = headers;
29 | this.body = body;
30 | this.certFilenames = certFilenames;
31 | this.timeout = timeout;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/PinchPackage.java:
--------------------------------------------------------------------------------
1 | package com.localz;
2 |
3 | import com.facebook.react.ReactPackage;
4 | import com.facebook.react.bridge.JavaScriptModule;
5 | import com.facebook.react.bridge.NativeModule;
6 | import com.facebook.react.bridge.ReactApplicationContext;
7 | import com.facebook.react.uimanager.ViewManager;
8 | import com.localz.RNPinch;
9 |
10 | import java.util.ArrayList;
11 | import java.util.Collections;
12 | import java.util.List;
13 |
14 | public class PinchPackage implements ReactPackage {
15 |
16 | public List> createJSModules() {
17 | return Collections.emptyList();
18 | }
19 |
20 | @Override
21 | public List createViewManagers(ReactApplicationContext reactContext) {
22 | return Collections.emptyList();
23 | }
24 |
25 | @Override
26 | public List createNativeModules(
27 | ReactApplicationContext reactContext) {
28 | List modules = new ArrayList<>();
29 | modules.add(new RNPinch(reactContext));
30 | return modules;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/pinch/utils/JsonUtil.java:
--------------------------------------------------------------------------------
1 | package com.localz.pinch.utils;
2 |
3 | import com.facebook.react.bridge.ReadableArray;
4 | import com.facebook.react.bridge.ReadableMap;
5 | import com.facebook.react.bridge.ReadableMapKeySetIterator;
6 |
7 | import org.json.JSONArray;
8 | import org.json.JSONException;
9 | import org.json.JSONObject;
10 |
11 | public class JsonUtil {
12 | public static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException {
13 | JSONArray array = new JSONArray();
14 | for (int i = 0; i < readableArray.size(); i++) {
15 | switch (readableArray.getType(i)) {
16 | case Null:
17 | break;
18 | case Boolean:
19 | array.put(readableArray.getBoolean(i));
20 | break;
21 | case Number:
22 | array.put(readableArray.getDouble(i));
23 | break;
24 | case String:
25 | array.put(readableArray.getString(i));
26 | break;
27 | case Map:
28 | array.put(convertReadableMapToJson(readableArray.getMap(i)));
29 | break;
30 | case Array:
31 | array.put(convertArrayToJson(readableArray.getArray(i)));
32 | break;
33 | }
34 | }
35 | return array;
36 | }
37 |
38 | public static JSONObject convertReadableMapToJson(ReadableMap readableMap) throws JSONException {
39 | JSONObject object = new JSONObject();
40 | ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
41 | while (iterator.hasNextKey()) {
42 | String key = iterator.nextKey();
43 | switch (readableMap.getType(key)) {
44 | case Null:
45 | object.put(key, JSONObject.NULL);
46 | break;
47 | case Boolean:
48 | object.put(key, readableMap.getBoolean(key));
49 | break;
50 | case Number:
51 | object.put(key, readableMap.getDouble(key));
52 | break;
53 | case String:
54 | object.put(key, readableMap.getString(key));
55 | break;
56 | case Map:
57 | object.put(key, convertReadableMapToJson(readableMap.getMap(key)));
58 | break;
59 | case Array:
60 | object.put(key, convertArrayToJson(readableMap.getArray(key)));
61 | break;
62 | }
63 | }
64 | return object;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/pinch/utils/KeyPinStoreUtil.java:
--------------------------------------------------------------------------------
1 | package com.localz.pinch.utils;
2 |
3 | import java.io.BufferedInputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.security.KeyManagementException;
7 | import java.security.KeyStore;
8 | import java.security.KeyStoreException;
9 | import java.security.NoSuchAlgorithmException;
10 | import java.security.cert.Certificate;
11 | import java.security.cert.CertificateException;
12 | import java.security.cert.CertificateFactory;
13 | import java.security.cert.X509Certificate;
14 | import java.util.HashMap;
15 | import java.util.Map;
16 |
17 | import javax.net.ssl.SSLContext;
18 | import javax.net.ssl.TrustManagerFactory;
19 |
20 | public class KeyPinStoreUtil {
21 |
22 | private static HashMap instances = new HashMap<>();
23 | private SSLContext sslContext = SSLContext.getInstance("TLS");
24 |
25 | public static synchronized KeyPinStoreUtil getInstance(String[] filenames) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
26 | if (filenames != null && instances.get(filenames) == null) {
27 | instances.put(filenames, new KeyPinStoreUtil(filenames));
28 | }
29 | return instances.get(filenames);
30 |
31 | }
32 |
33 | private KeyPinStoreUtil(String[] filenames) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
34 | CertificateFactory cf = CertificateFactory.getInstance("X.509");
35 |
36 | // Create a KeyStore for our trusted CAs
37 | String keyStoreType = KeyStore.getDefaultType();
38 | KeyStore keyStore = KeyStore.getInstance(keyStoreType);
39 | keyStore.load(null, null);
40 |
41 | for (String filename : filenames) {
42 | InputStream caInput = new BufferedInputStream(this.getClass().getClassLoader().getResourceAsStream("assets/" + filename + ".cer"));
43 | Certificate ca;
44 | try {
45 | ca = cf.generateCertificate(caInput);
46 | System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
47 | } finally {
48 | caInput.close();
49 | }
50 |
51 | keyStore.setCertificateEntry(filename, ca);
52 | }
53 |
54 | // Create a TrustManager that trusts the CAs in our KeyStore
55 | String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
56 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
57 | tmf.init(keyStore);
58 |
59 | sslContext.init(null, tmf.getTrustManagers(), null);
60 | }
61 |
62 | public SSLContext getContext() {
63 | return sslContext;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { NativeModules, Platform } from 'react-native';
4 | var Q = require('q');
5 |
6 | var RNPinch = {
7 | fetch: function (url, obj, callback) {
8 | var deferred = Q.defer();
9 | NativeModules.RNPinch.fetch(url, obj, (err, res) => {
10 | //var err = {...err};
11 | if (err) {
12 | switch (getKeyByValue(err.code)) {
13 | case 'kCFURLErrorTimedOut':
14 | err.reason = 'Timeout';
15 | break;
16 | case 'kCFURLErrorUnsupportedURL':
17 | case 'kCFURLErrorCannotFindHost':
18 | case 'kCFURLErrorCannotConnectToHost':
19 | case 'kCFURLErrorNetworkConnectionLost':
20 | case 'kCFURLErrorDNSLookupFailed':
21 | case 'kCFURLErrorHTTPTooManyRedirects':
22 | case 'kCFURLErrorResourceUnavailable':
23 | case 'kCFURLErrorNotConnectedToInternet':
24 | err.reason = 'Network error';
25 | break;
26 | default:
27 | err.reason = 'Unknown error';
28 | break;
29 | }
30 | deferred.reject(err);
31 | } else {
32 | res.json = function () {
33 | return Q.fcall(function () {
34 | return JSON.parse(res.bodyString);
35 | });
36 | };
37 | res.text = function () {
38 | return Q.fcall(function () {
39 | return res.bodyString;
40 | });
41 | };
42 | res.url = url;
43 | deferred.resolve(res);
44 | }
45 |
46 | deferred.promise.nodeify(callback);
47 | });
48 | return deferred.promise;
49 | }
50 | };
51 |
52 | const obj = {
53 | kCFURLErrorUnknown: -998,
54 | kCFURLErrorCancelled: -999,
55 | kCFURLErrorBadURL: -1000,
56 | kCFURLErrorTimedOut: -1001,
57 | kCFURLErrorUnsupportedURL: -1002,
58 | kCFURLErrorCannotFindHost: -1003,
59 | kCFURLErrorCannotConnectToHost: -1004,
60 | kCFURLErrorNetworkConnectionLost: -1005,
61 | kCFURLErrorDNSLookupFailed: -1006,
62 | kCFURLErrorHTTPTooManyRedirects: -1007,
63 | kCFURLErrorResourceUnavailable: -1008,
64 | kCFURLErrorNotConnectedToInternet: -1009,
65 | kCFURLErrorRedirectToNonExistentLocation: -1010,
66 | kCFURLErrorBadServerResponse: -1011,
67 | kCFURLErrorUserCancelledAuthentication: -1012,
68 | kCFURLErrorUserAuthenticationRequired: -1013,
69 | kCFURLErrorZeroByteResource: -1014,
70 | kCFURLErrorCannotDecodeRawData: -1015,
71 | kCFURLErrorCannotDecodeContentData: -1016,
72 | kCFURLErrorCannotParseResponse: -1017,
73 | kCFURLErrorInternationalRoamingOff: -1018,
74 | kCFURLErrorCallIsActive: -1019,
75 | kCFURLErrorDataNotAllowed: -1020,
76 | kCFURLErrorRequestBodyStreamExhausted: -1021,
77 | kCFURLErrorFileDoesNotExist: -1100,
78 | kCFURLErrorFileIsDirectory: -1101,
79 | kCFURLErrorNoPermissionsToReadFile: -1102,
80 | kCFURLErrorDataLengthExceedsMaximum: -1103,
81 | }
82 |
83 | function getKeyByValue(value) {
84 | return Object.keys(obj).find(key => obj[key] === value);
85 | }
86 |
87 | module.exports = RNPinch;
88 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/pinch/utils/HttpUtil.java:
--------------------------------------------------------------------------------
1 | package com.localz.pinch.utils;
2 |
3 | import android.util.Log;
4 |
5 | import com.facebook.react.bridge.Arguments;
6 | import com.facebook.react.bridge.ReadableMapKeySetIterator;
7 | import com.facebook.react.bridge.WritableMap;
8 |
9 | import java.net.HttpURLConnection;
10 | import java.net.URI;
11 | import java.net.URISyntaxException;
12 |
13 | import com.localz.pinch.models.HttpRequest;
14 | import com.localz.pinch.models.HttpResponse;
15 |
16 | import org.json.JSONException;
17 | import org.json.JSONObject;
18 |
19 | import java.io.BufferedReader;
20 | import java.io.IOException;
21 | import java.io.InputStreamReader;
22 | import java.io.InputStream;
23 | import java.io.OutputStream;
24 | import java.net.URL;
25 | import java.security.KeyManagementException;
26 | import java.security.KeyStoreException;
27 | import java.security.NoSuchAlgorithmException;
28 | import java.security.cert.CertificateException;
29 | import java.util.Iterator;
30 | import java.util.List;
31 | import java.util.Map;
32 |
33 | import javax.net.ssl.HttpsURLConnection;
34 |
35 | public class HttpUtil {
36 | private static final String DEFAULT_CONTENT_TYPE = "application/json";
37 |
38 | private String getResponseBody(InputStream responseStream) throws IOException {
39 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(responseStream));
40 | StringBuilder sb = new StringBuilder();
41 | String line;
42 |
43 | while ((line = bufferedReader.readLine()) != null) {
44 | sb.append(line);
45 | }
46 | bufferedReader.close();
47 |
48 | return sb.toString();
49 | }
50 |
51 | private WritableMap getResponseHeaders(HttpURLConnection connection) {
52 | WritableMap jsonHeaders = Arguments.createMap();
53 | Map> headerMap = connection.getHeaderFields();
54 |
55 | for (Map.Entry> entry : headerMap.entrySet()) {
56 | if (entry.getKey() != null) {
57 | jsonHeaders.putString(entry.getKey(), entry.getValue().get(0));
58 | }
59 | }
60 |
61 | return jsonHeaders;
62 | }
63 |
64 | private HttpURLConnection prepareRequestHeaders(HttpURLConnection connection, JSONObject headers) throws JSONException {
65 | connection.setRequestProperty("Content-Type", DEFAULT_CONTENT_TYPE);
66 | connection.setRequestProperty("Accept", DEFAULT_CONTENT_TYPE);
67 |
68 | if (headers != null) {
69 | Iterator iterator = headers.keys();
70 | while (iterator.hasNext()) {
71 | String nextKey = iterator.next();
72 | connection.setRequestProperty(nextKey, headers.get(nextKey).toString());
73 | }
74 | }
75 |
76 | return connection;
77 | }
78 |
79 | private HttpURLConnection prepareRequest(HttpRequest request)
80 | throws IOException, KeyStoreException, CertificateException, KeyManagementException, NoSuchAlgorithmException, JSONException {
81 | HttpURLConnection connection;
82 | URL url = new URL(request.endpoint);
83 | String method = request.method.toUpperCase();
84 |
85 | if(request.endpoint.startsWith("https") && !request.ignore_ssl) {
86 | HttpsURLConnection httpsConnection = (HttpsURLConnection) url.openConnection();
87 | if (request.certFilenames != null && request.certFilenames.length > 0) {
88 | httpsConnection.setSSLSocketFactory(KeyPinStoreUtil.getInstance(request.certFilenames).getContext().getSocketFactory());
89 | }
90 | connection = httpsConnection;
91 | } else if(request.endpoint.startsWith("https") && request.ignore_ssl) {
92 | HttpsURLConnection httpsConnection = (HttpsURLConnection) url.openConnection();
93 | connection = httpsConnection;
94 | }else{
95 | connection = (HttpURLConnection) url.openConnection();
96 | }
97 | connection.setRequestMethod(method);
98 |
99 | connection = prepareRequestHeaders(connection, request.headers);
100 |
101 | connection.setRequestProperty("Accept-Charset", "UTF-8");
102 | connection.setAllowUserInteraction(false);
103 | connection.setConnectTimeout(request.timeout);
104 | connection.setReadTimeout(request.timeout);
105 |
106 | if (request.body != null && (method.equals("POST") || method.equals("PUT") || method.equals("PATCH"))) {
107 | // Set the content length of the body.
108 | connection.setRequestProperty("Content-length", request.body.getBytes().length + "");
109 | connection.setDoInput(true);
110 | connection.setDoOutput(true);
111 | connection.setUseCaches(false);
112 |
113 | // Send the JSON as body of the request.
114 | OutputStream outputStream = connection.getOutputStream();
115 | outputStream.write(request.body.getBytes("UTF-8"));
116 | outputStream.close();
117 | }
118 |
119 | return connection;
120 | }
121 |
122 | private InputStream prepareResponseStream(HttpURLConnection connection) throws IOException {
123 | try {
124 | return connection.getInputStream();
125 | } catch (IOException e) {
126 | return connection.getErrorStream();
127 | }
128 | }
129 |
130 | public HttpResponse sendHttpRequest(HttpRequest request)
131 | throws IOException, KeyStoreException, CertificateException, KeyManagementException, NoSuchAlgorithmException, JSONException {
132 | InputStream responseStream = null;
133 | HttpResponse response = new HttpResponse();
134 | HttpURLConnection connection;
135 | int status;
136 | String statusText;
137 |
138 | try {
139 | connection = prepareRequest(request);
140 |
141 | connection.connect();
142 |
143 | status = connection.getResponseCode();
144 | statusText = connection.getResponseMessage();
145 | responseStream = prepareResponseStream(connection);
146 |
147 | response.statusCode = status;
148 | response.statusText = statusText;
149 | response.bodyString = getResponseBody(responseStream);
150 | response.headers = getResponseHeaders(connection);
151 |
152 | return response;
153 | } finally {
154 | if (responseStream != null) {
155 | responseStream.close();
156 | }
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This library is created because currently react-native-pinch library is not maintaining actively. Also this library supports HTTP and HTTPS.
2 |
3 | # React Native Pinch New 👌
4 |
5 | Callback and promise based HTTP client that supports SSL pinning for React Native.
6 |
7 | ## Installation
8 |
9 | Using NPM:
10 | ```
11 | npm install react-native-pinch-new
12 | ```
13 |
14 | Using Yarn:
15 | ```
16 | yarn add react-native-pinch-new
17 | ```
18 | ## Supports the new Autolinking
19 |
20 | ## Automatically link (Only recommended when autolinking failed)
21 |
22 | #### With React Native 0.27+
23 |
24 | ```shell
25 | react-native link react-native-pinch-new
26 | ```
27 |
28 | #### With older versions of React Native
29 |
30 | You need [`rnpm`](https://github.com/rnpm/rnpm) (`npm install -g rnpm`)
31 |
32 | ```shell
33 | rnpm link react-native-pinch-new
34 | ```
35 |
36 | ## Manually link
37 |
38 | ### iOS (via Cocoa Pods)
39 | Add the following line to your build targets in your `Podfile`
40 |
41 | `pod 'react-native-pinch-new', :path => '../node_modules/react-native-pinch-new'`
42 |
43 | Then run `pod install`
44 |
45 | ### Android
46 |
47 | - in `android/app/build.gradle`:
48 |
49 | ```diff
50 | dependencies {
51 | ...
52 | compile "com.facebook.react:react-native:+" // From node_modules
53 | + compile project(':react-native-pinch')
54 | }
55 | ```
56 |
57 | - in `android/settings.gradle`:
58 |
59 | ```diff
60 | ...
61 | include ':app'
62 | + include ':react-native-pinch'
63 | + project(':react-native-pinch').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-pinch-new/android')
64 | ```
65 |
66 | #### With React Native 0.29+
67 |
68 | - in `MainApplication.java`:
69 |
70 | ```diff
71 | + import com.localz.PinchPackage;
72 |
73 | public class MainApplication extends Application implements ReactApplication {
74 | //......
75 |
76 | @Override
77 | protected List getPackages() {
78 | return Arrays.asList(
79 | + new PinchPackage(),
80 | new MainReactPackage()
81 | );
82 | }
83 |
84 | ......
85 | }
86 | ```
87 |
88 | #### With older versions of React Native:
89 |
90 | - in `MainActivity.java`:
91 |
92 | ```diff
93 | + import com.localz.PinchPackage;
94 |
95 | public class MainActivity extends ReactActivity {
96 | ......
97 |
98 | @Override
99 | protected List getPackages() {
100 | return Arrays.asList(
101 | + new PinchPackage(),
102 | new MainReactPackage()
103 | );
104 | }
105 | }
106 | ```
107 |
108 | ## Adding certificates
109 |
110 | Before you can make requests using SSL pinning, you first need to add your `.cer` files to your project's assets.
111 |
112 | ### Android
113 |
114 | - Place your `.cer` files under `src/main/assets/`.
115 |
116 | ### iOS
117 |
118 | - Place your `.der` files in your iOS Project. Don't forget to add them in your `Build Phases > Copy Bundle Resources`, in Xcode.
119 |
120 |
121 | ## Example
122 | *Examples are using the ES6 standard*
123 |
124 | Requests can be made by using the `fetch(url[, config, [callback]])` method of Pinch.
125 |
126 | ### Using Promises
127 | ```javascript
128 | import pinch from 'react-native-pinch-new';
129 |
130 | pinch.fetch('https://my-api.com/v1/endpoint', {
131 | method: 'post',
132 | headers: { customHeader: 'customValue' },
133 | body: '{"firstName": "Jake", "lastName": "Moxey"}',
134 | timeoutInterval: 10000 // timeout after 10 seconds
135 | sslPinning: {
136 | cert: 'cert-file-name', // cert file name without the `.cer`
137 | certs: ['cert-file-name-1', 'cert-file-name-2'], // optionally specify multiple certificates
138 | }
139 | })
140 | .then(res => console.log(`We got your response! Response - ${res}`))
141 | .catch(err => console.log(`Whoopsy doodle! Error - ${err}`))
142 | ```
143 |
144 | ### Using Callbacks
145 | ```javascript
146 | import pinch from 'react-native-pinch-new';
147 |
148 | pinch.fetch('https://my-api.com/v1/endpoint', {
149 | method: 'post',
150 | headers: { customHeader: 'customValue' },
151 | body: '{"firstName": "Jake", "lastName": "Moxey"}',
152 | timeoutInterval: 10000 // timeout after 10 seconds
153 | sslPinning: {
154 | cert: 'cert-file-name', // cert file name without the `.cer`
155 | certs: ['cert-file-name-1', 'cert-file-name-2'], // optionally specify multiple certificates
156 | }
157 | }, (err, res) => {
158 | if (err) {
159 | console.error(`Whoopsy doodle! Error - ${err}`);
160 | return null;
161 | }
162 | console.log(`We got your response! Response - ${res}`);
163 | })
164 | ```
165 |
166 | ### Skipping validation
167 |
168 | In original library, android did not support http. In order to achieve this, you only need to pass a valid http url.
169 |
170 | ```javascript
171 | import pinch from 'react-native-pinch-new';
172 |
173 | pinch.fetch('https://my-api.com/v1/endpoint', {
174 | method: 'post',
175 | headers: { customHeader: 'customValue' },
176 | body: '{"firstName": "Jake", "lastName": "Moxey"}',
177 | timeoutInterval: 10000 // timeout after 10 seconds
178 | ignore_ssl:true, // This can be used to ignore SSL pinning
179 | sslPinning: {} // omit the `cert` or `certs` key, `sslPinning` can be ommited as well
180 | })
181 | ```
182 |
183 | ## Response Schema
184 | ```javascript
185 | {
186 | bodyString: '',
187 |
188 | headers: {},
189 |
190 | status: 200,
191 |
192 | statusText: 'OK'
193 | }
194 | ```
195 |
196 | ## Testing
197 |
198 | ### With jest
199 |
200 | Using [fetch-mock](http://www.wheresrhys.co.uk/fetch-mock/) here, but nock or any other fetch polyfill would work.
201 |
202 | ```js
203 | # __mocks__/react-native-pinch-new.js
204 | import fetchMock from 'fetch-mock';
205 |
206 | export default {
207 | fetch: fetchMock.sandbox(), // mock pinch's fetch with the sandbox version
208 | };
209 | ```
210 |
211 | ```js
212 | # __tests__/store.js
213 | import configureMockStore from 'redux-mock-store';
214 | import thunk from 'redux-thunk';
215 | import pinch from 'react-native-pinch-new'; // actually the sandbox from fetch-mock
216 |
217 | import { fetchFoos } from './path/to/store/actions';
218 |
219 | jest.mock('react-native-pinch-new');
220 |
221 | const middlewares = [thunk];
222 | const mockStore = configureMockStore(middlewares);
223 |
224 | afterEach(() => {
225 | pinch.fetch.reset();
226 | pinch.fetch.restore();
227 | });
228 |
229 | describe('fetchFoos', () => {
230 | it('creates FOO_BAR when fetching foos is done', () => {
231 | pinch.fetch.get(/^\/foos/, { foos: [] });
232 | const store = mockStore(defaultState);
233 |
234 | return store.dispatch(fetchFoos()).then(() => {
235 | expect(store.getActions()).toEqual(expect.arrayContaining(
236 | [expect.objectContaining({ type: FOO_BAR })],
237 | ));
238 | });
239 | });
240 | });
241 | ```
242 |
--------------------------------------------------------------------------------
/android/src/main/java/com/localz/RNPinch.java:
--------------------------------------------------------------------------------
1 | package com.localz;
2 |
3 | import android.os.AsyncTask;
4 | import android.util.Log;
5 | import android.content.pm.PackageManager;
6 | import android.content.pm.PackageInfo;
7 | import android.content.pm.ApplicationInfo;
8 | import android.content.pm.PackageManager.NameNotFoundException;
9 |
10 | import com.facebook.react.bridge.Arguments;
11 | import com.facebook.react.bridge.Callback;
12 | import com.facebook.react.bridge.ReactApplicationContext;
13 | import com.facebook.react.bridge.ReactContextBaseJavaModule;
14 | import com.facebook.react.bridge.ReactMethod;
15 | import com.facebook.react.bridge.ReadableMap;
16 | import com.facebook.react.bridge.ReadableArray;
17 | import com.facebook.react.bridge.UnexpectedNativeTypeException;
18 | import com.facebook.react.bridge.WritableMap;
19 |
20 | import com.localz.pinch.models.HttpRequest;
21 | import com.localz.pinch.models.HttpResponse;
22 | import com.localz.pinch.utils.HttpUtil;
23 | import com.localz.pinch.utils.JsonUtil;
24 |
25 | import org.json.JSONException;
26 | import org.json.JSONObject;
27 |
28 | import java.io.IOException;
29 | import java.net.SocketTimeoutException;
30 | import java.net.ConnectException;
31 | import java.net.UnknownHostException;
32 | import java.security.KeyManagementException;
33 | import java.security.KeyStoreException;
34 | import java.security.NoSuchAlgorithmException;
35 | import java.security.cert.CertificateException;
36 |
37 | public class RNPinch extends ReactContextBaseJavaModule {
38 |
39 | private static final String OPT_METHOD_KEY = "method";
40 | private static final String OPT_HEADER_KEY = "headers";
41 | private static final String OPT_BODY_KEY = "body";
42 | private static final String OPT_IGNORE_SSL_KEY = "ignore_ssl";
43 | private static final String OPT_SSL_PINNING_KEY = "sslPinning";
44 | private static final String OPT_TIMEOUT_KEY = "timeoutInterval";
45 |
46 | private HttpUtil httpUtil;
47 | private String packageName = null;
48 | private String displayName = null;
49 | private String version = null;
50 | private String versionCode = null;
51 |
52 | public RNPinch(ReactApplicationContext reactContext) {
53 | super(reactContext);
54 | httpUtil = new HttpUtil();
55 | try {
56 | PackageManager pManager = reactContext.getPackageManager();
57 | packageName = reactContext.getPackageName();
58 | PackageInfo pInfo = pManager.getPackageInfo(packageName, 0);
59 | ApplicationInfo aInfo = pManager.getApplicationInfo(packageName, 0);
60 | displayName = pManager.getApplicationLabel(aInfo).toString();
61 | version = pInfo.versionName;
62 | versionCode = String.valueOf(pInfo.versionCode);
63 | } catch (NameNotFoundException nnfe) {
64 | System.out.println("RNAppInfo: package name not found");
65 | }
66 | }
67 |
68 | @Override
69 | public String getName() {
70 | return "RNPinch";
71 | }
72 |
73 | @ReactMethod
74 | public void fetch(String endpoint, ReadableMap opts, Callback callback) {
75 | new FetchTask(opts, callback).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, endpoint);
76 | }
77 |
78 | private class FetchTask extends AsyncTask {
79 | private ReadableMap opts;
80 | private Callback callback;
81 |
82 | public FetchTask(ReadableMap opts, Callback callback) {
83 | this.opts = opts;
84 | this.callback = callback;
85 | }
86 |
87 | @Override
88 | protected WritableMap doInBackground(String... endpoint) {
89 |
90 | try {
91 | WritableMap response = Arguments.createMap();
92 | HttpRequest request = new HttpRequest(endpoint[0]);
93 | Boolean ignoressl = false;
94 |
95 | if (opts.hasKey(OPT_BODY_KEY)) {
96 | request.body = opts.getString(OPT_BODY_KEY);
97 | }
98 | if (opts.hasKey(OPT_METHOD_KEY)) {
99 | request.method = opts.getString(OPT_METHOD_KEY);
100 | }
101 | if (opts.hasKey(OPT_HEADER_KEY)) {
102 | request.headers = JsonUtil.convertReadableMapToJson(opts.getMap(OPT_HEADER_KEY));
103 | }
104 | if (opts.hasKey(OPT_IGNORE_SSL_KEY)) {
105 | ignoressl = opts.getBoolean(OPT_IGNORE_SSL_KEY);
106 | request.ignore_ssl = opts.getBoolean(OPT_IGNORE_SSL_KEY);
107 | }
108 | if (opts.hasKey(OPT_SSL_PINNING_KEY) && !ignoressl) {
109 | if (opts.getMap(OPT_SSL_PINNING_KEY).hasKey("cert")) {
110 | String fileName = opts.getMap(OPT_SSL_PINNING_KEY).getString("cert");
111 | request.certFilenames = new String[]{fileName};
112 | } else if (opts.getMap(OPT_SSL_PINNING_KEY).hasKey("certs")) {
113 | ReadableArray certsStrings = opts.getMap(OPT_SSL_PINNING_KEY).getArray("certs");
114 | String[] certs = new String[certsStrings.size()];
115 | for (int i = 0; i < certsStrings.size(); i++) {
116 | certs[i] = certsStrings.getString(i);
117 | }
118 | request.certFilenames = certs;
119 | }
120 | }
121 | if (opts.hasKey(OPT_TIMEOUT_KEY)) {
122 | request.timeout = opts.getInt(OPT_TIMEOUT_KEY);
123 | }
124 |
125 | HttpResponse httpResponse = httpUtil.sendHttpRequest(request);
126 |
127 | response.putInt("status", httpResponse.statusCode);
128 | response.putString("statusText", httpResponse.statusText);
129 | response.putString("bodyString", httpResponse.bodyString);
130 | response.putMap("headers", httpResponse.headers);
131 |
132 | return response;
133 | }catch(SocketTimeoutException STE){
134 | WritableMap er = Arguments.createMap();
135 | er.putString("message", "The request timed out.");
136 | er.putInt("code", -1001);
137 | return er;
138 | }catch(ConnectException | UnknownHostException Ex){
139 | WritableMap er = Arguments.createMap();
140 | er.putString("message", Ex.toString());
141 | er.putInt("code", -1003);
142 | return er;
143 | }
144 | catch(JSONException | IOException | UnexpectedNativeTypeException | KeyStoreException | CertificateException | KeyManagementException | NoSuchAlgorithmException e) {
145 | WritableMap er = Arguments.createMap();
146 | er.putString("message", e.toString());
147 | er.putInt("code", -998);
148 | return er;
149 | }
150 | }
151 |
152 | @Override
153 | protected void onPostExecute(WritableMap response) {
154 |
155 | if (response.hasKey("message")) {
156 | callback.invoke(response, null);
157 | } else {
158 | callback.invoke(null, response);
159 | }
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/RNPinch/RNPinch.m:
--------------------------------------------------------------------------------
1 | //
2 | // RNNativeFetch.m
3 | // medipass
4 | //
5 | // Created by Paul Wong on 13/10/16.
6 | // Copyright © 2016 Localz. All rights reserved.
7 | //
8 |
9 | #import "RNPinch.h"
10 | #import "RCTBridge.h"
11 |
12 | @interface RNPinchException : NSException
13 | @end
14 | @implementation RNPinchException
15 | @end
16 |
17 | // private delegate for verifying certs
18 | @interface NSURLSessionSSLPinningDelegate:NSObject
19 |
20 | - (id)initWithCertNames:(NSArray *)certNames;
21 |
22 | @property (nonatomic, strong) NSArray *certNames;
23 |
24 | @end
25 |
26 | @implementation NSURLSessionSSLPinningDelegate
27 |
28 | - (id)initWithCertNames:(NSArray *)certNames {
29 | if (self = [super init]) {
30 | _certNames = certNames;
31 | }
32 | return self;
33 | }
34 |
35 | - (NSArray *)pinnedCertificateData {
36 | NSMutableArray *localCertData = [NSMutableArray array];
37 | for (NSString* certName in self.certNames) {
38 | NSString *cerPath = [[NSBundle mainBundle] pathForResource:certName ofType:@"der"];
39 | if (cerPath == nil) {
40 | @throw [[RNPinchException alloc]
41 | initWithName:@"CertificateError"
42 | reason:@"Can not load certicate given, check it's in the app resources."
43 | userInfo:nil];
44 | }
45 | [localCertData addObject:[NSData dataWithContentsOfFile:cerPath]];
46 | }
47 |
48 | NSMutableArray *pinnedCertificates = [NSMutableArray array];
49 | for (NSData *certificateData in localCertData) {
50 | [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
51 | }
52 | return pinnedCertificates;
53 | }
54 |
55 | - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
56 |
57 | if ([[[challenge protectionSpace] authenticationMethod] isEqualToString:NSURLAuthenticationMethodServerTrust]) {
58 | NSString *domain = challenge.protectionSpace.host;
59 | SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
60 |
61 | NSArray *policies = @[(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
62 |
63 | SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
64 | // setup
65 | SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)self.pinnedCertificateData);
66 | SecTrustResultType result;
67 |
68 | // evaluate
69 | OSStatus errorCode = SecTrustEvaluate(serverTrust, &result);
70 |
71 | BOOL evaluatesAsTrusted = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
72 | if (errorCode == errSecSuccess && evaluatesAsTrusted) {
73 | NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
74 | completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
75 | } else {
76 | completionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, NULL);
77 | }
78 | } else {
79 | completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
80 | }
81 | }
82 |
83 | @end
84 |
85 | @interface RNPinch()
86 |
87 | @property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
88 |
89 | @end
90 |
91 | @implementation RNPinch
92 | RCT_EXPORT_MODULE();
93 |
94 | - (instancetype)init
95 | {
96 | self = [super init];
97 | if (self) {
98 | self.sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
99 | self.sessionConfig.HTTPCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
100 | }
101 | return self;
102 | }
103 |
104 | + (BOOL)requiresMainQueueSetup
105 | {
106 | return NO;
107 | }
108 |
109 | RCT_EXPORT_METHOD(fetch:(NSString *)url obj:(NSDictionary *)obj callback:(RCTResponseSenderBlock)callback) {
110 | NSURL *u = [NSURL URLWithString:url];
111 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:u];
112 |
113 | NSURLSession *session;
114 | if (obj) {
115 | if (obj[@"method"]) {
116 | [request setHTTPMethod:obj[@"method"]];
117 | }
118 | if (obj[@"timeoutInterval"]) {
119 | [request setTimeoutInterval:[obj[@"timeoutInterval"] doubleValue] / 1000];
120 | }
121 | if (obj[@"headers"] && [obj[@"headers"] isKindOfClass:[NSDictionary class]]) {
122 | NSMutableDictionary *m = [obj[@"headers"] mutableCopy];
123 | for (NSString *key in [m allKeys]) {
124 | if (![m[key] isKindOfClass:[NSString class]]) {
125 | m[key] = [m[key] stringValue];
126 | }
127 | }
128 | [request setAllHTTPHeaderFields:m];
129 | }
130 | if (obj[@"body"]) {
131 | NSData *data = [obj[@"body"] dataUsingEncoding:NSUTF8StringEncoding];
132 | [request setHTTPBody:data];
133 | }
134 | }
135 | if (obj && obj[@"ignore_ssl"] && ![[obj objectForKey:@"ignore_ssl"] boolValue] && obj[@"sslPinning"] && obj[@"sslPinning"][@"cert"]) {
136 | NSURLSessionSSLPinningDelegate *delegate = [[NSURLSessionSSLPinningDelegate alloc] initWithCertNames:@[obj[@"sslPinning"][@"cert"]]];
137 | session = [NSURLSession sessionWithConfiguration:self.sessionConfig delegate:delegate delegateQueue:[NSOperationQueue mainQueue]];
138 | } else if (obj && obj[@"ignore_ssl"] && ![[obj objectForKey:@"ignore_ssl"] boolValue] && obj[@"sslPinning"] && obj[@"sslPinning"][@"certs"]) {
139 | // load all certs
140 | NSURLSessionSSLPinningDelegate *delegate = [[NSURLSessionSSLPinningDelegate alloc] initWithCertNames:obj[@"sslPinning"][@"certs"]];
141 | session = [NSURLSession sessionWithConfiguration:self.sessionConfig delegate:delegate delegateQueue:[NSOperationQueue mainQueue]];
142 | } else {
143 | session = [NSURLSession sessionWithConfiguration:self.sessionConfig];
144 | }
145 |
146 | __block NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
147 | if (!error) {
148 | dispatch_async(dispatch_get_main_queue(), ^{
149 | NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
150 | NSInteger statusCode = httpResp.statusCode;
151 | NSString *bodyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
152 | NSString *statusText = [NSHTTPURLResponse localizedStringForStatusCode:httpResp.statusCode];
153 |
154 | NSDictionary *res = @{
155 | @"status": @(statusCode),
156 | @"headers": httpResp.allHeaderFields,
157 | @"bodyString": bodyString,
158 | @"statusText": statusText
159 | };
160 | callback(@[[NSNull null], res]);
161 | });
162 | } else {
163 | dispatch_async(dispatch_get_main_queue(), ^{
164 | NSInteger code = [error code];
165 | callback(@[@{@"message":error.localizedDescription,@"code":@(code) }, [NSNull null]]);
166 | });
167 | }
168 | }];
169 |
170 | [dataTask resume];
171 | }
172 |
173 | @end
174 |
--------------------------------------------------------------------------------
/RNPinch.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | DA08C4AF1DAF39C600714E34 /* RNPinch.m in Sources */ = {isa = PBXBuildFile; fileRef = DA08C4AE1DAF39C600714E34 /* RNPinch.m */; };
11 | DA08C4B01DAF39C600714E34 /* RNPinch.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = DA08C4AD1DAF39C600714E34 /* RNPinch.h */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXCopyFilesBuildPhase section */
15 | DA08C4A81DAF39C600714E34 /* CopyFiles */ = {
16 | isa = PBXCopyFilesBuildPhase;
17 | buildActionMask = 2147483647;
18 | dstPath = "include/$(PRODUCT_NAME)";
19 | dstSubfolderSpec = 16;
20 | files = (
21 | DA08C4B01DAF39C600714E34 /* RNPinch.h in CopyFiles */,
22 | );
23 | runOnlyForDeploymentPostprocessing = 0;
24 | };
25 | /* End PBXCopyFilesBuildPhase section */
26 |
27 | /* Begin PBXFileReference section */
28 | DA08C4AA1DAF39C600714E34 /* libRNPinch.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNPinch.a; sourceTree = BUILT_PRODUCTS_DIR; };
29 | DA08C4AD1DAF39C600714E34 /* RNPinch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNPinch.h; sourceTree = ""; };
30 | DA08C4AE1DAF39C600714E34 /* RNPinch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNPinch.m; sourceTree = ""; };
31 | /* End PBXFileReference section */
32 |
33 | /* Begin PBXFrameworksBuildPhase section */
34 | DA08C4A71DAF39C600714E34 /* Frameworks */ = {
35 | isa = PBXFrameworksBuildPhase;
36 | buildActionMask = 2147483647;
37 | files = (
38 | );
39 | runOnlyForDeploymentPostprocessing = 0;
40 | };
41 | /* End PBXFrameworksBuildPhase section */
42 |
43 | /* Begin PBXGroup section */
44 | DA08C4A11DAF39C600714E34 = {
45 | isa = PBXGroup;
46 | children = (
47 | DA08C4AC1DAF39C600714E34 /* RNPinch */,
48 | DA08C4AB1DAF39C600714E34 /* Products */,
49 | );
50 | sourceTree = "";
51 | usesTabs = 0;
52 | };
53 | DA08C4AB1DAF39C600714E34 /* Products */ = {
54 | isa = PBXGroup;
55 | children = (
56 | DA08C4AA1DAF39C600714E34 /* libRNPinch.a */,
57 | );
58 | name = Products;
59 | sourceTree = "";
60 | };
61 | DA08C4AC1DAF39C600714E34 /* RNPinch */ = {
62 | isa = PBXGroup;
63 | children = (
64 | DA08C4AD1DAF39C600714E34 /* RNPinch.h */,
65 | DA08C4AE1DAF39C600714E34 /* RNPinch.m */,
66 | );
67 | path = RNPinch;
68 | sourceTree = "";
69 | };
70 | /* End PBXGroup section */
71 |
72 | /* Begin PBXNativeTarget section */
73 | DA08C4A91DAF39C600714E34 /* RNPinch */ = {
74 | isa = PBXNativeTarget;
75 | buildConfigurationList = DA08C4B31DAF39C600714E34 /* Build configuration list for PBXNativeTarget "RNPinch" */;
76 | buildPhases = (
77 | DA08C4A61DAF39C600714E34 /* Sources */,
78 | DA08C4A71DAF39C600714E34 /* Frameworks */,
79 | DA08C4A81DAF39C600714E34 /* CopyFiles */,
80 | );
81 | buildRules = (
82 | );
83 | dependencies = (
84 | );
85 | name = RNPinch;
86 | productName = RNPinch;
87 | productReference = DA08C4AA1DAF39C600714E34 /* libRNPinch.a */;
88 | productType = "com.apple.product-type.library.static";
89 | };
90 | /* End PBXNativeTarget section */
91 |
92 | /* Begin PBXProject section */
93 | DA08C4A21DAF39C600714E34 /* Project object */ = {
94 | isa = PBXProject;
95 | attributes = {
96 | LastUpgradeCheck = 0800;
97 | ORGANIZATIONNAME = Localz;
98 | TargetAttributes = {
99 | DA08C4A91DAF39C600714E34 = {
100 | CreatedOnToolsVersion = 8.0;
101 | ProvisioningStyle = Automatic;
102 | };
103 | };
104 | };
105 | buildConfigurationList = DA08C4A51DAF39C600714E34 /* Build configuration list for PBXProject "RNPinch" */;
106 | compatibilityVersion = "Xcode 3.2";
107 | developmentRegion = English;
108 | hasScannedForEncodings = 0;
109 | knownRegions = (
110 | en,
111 | );
112 | mainGroup = DA08C4A11DAF39C600714E34;
113 | productRefGroup = DA08C4AB1DAF39C600714E34 /* Products */;
114 | projectDirPath = "";
115 | projectRoot = "";
116 | targets = (
117 | DA08C4A91DAF39C600714E34 /* RNPinch */,
118 | );
119 | };
120 | /* End PBXProject section */
121 |
122 | /* Begin PBXSourcesBuildPhase section */
123 | DA08C4A61DAF39C600714E34 /* Sources */ = {
124 | isa = PBXSourcesBuildPhase;
125 | buildActionMask = 2147483647;
126 | files = (
127 | DA08C4AF1DAF39C600714E34 /* RNPinch.m in Sources */,
128 | );
129 | runOnlyForDeploymentPostprocessing = 0;
130 | };
131 | /* End PBXSourcesBuildPhase section */
132 |
133 | /* Begin XCBuildConfiguration section */
134 | DA08C4B11DAF39C600714E34 /* Debug */ = {
135 | isa = XCBuildConfiguration;
136 | buildSettings = {
137 | ALWAYS_SEARCH_USER_PATHS = NO;
138 | CLANG_ANALYZER_NONNULL = YES;
139 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
140 | CLANG_CXX_LIBRARY = "libc++";
141 | CLANG_ENABLE_MODULES = YES;
142 | CLANG_ENABLE_OBJC_ARC = YES;
143 | CLANG_WARN_BOOL_CONVERSION = YES;
144 | CLANG_WARN_CONSTANT_CONVERSION = YES;
145 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
146 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
147 | CLANG_WARN_EMPTY_BODY = YES;
148 | CLANG_WARN_ENUM_CONVERSION = YES;
149 | CLANG_WARN_INFINITE_RECURSION = YES;
150 | CLANG_WARN_INT_CONVERSION = YES;
151 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
152 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
153 | CLANG_WARN_UNREACHABLE_CODE = YES;
154 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
155 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
156 | COPY_PHASE_STRIP = NO;
157 | DEBUG_INFORMATION_FORMAT = dwarf;
158 | ENABLE_STRICT_OBJC_MSGSEND = YES;
159 | ENABLE_TESTABILITY = YES;
160 | GCC_C_LANGUAGE_STANDARD = gnu99;
161 | GCC_DYNAMIC_NO_PIC = NO;
162 | GCC_NO_COMMON_BLOCKS = YES;
163 | GCC_OPTIMIZATION_LEVEL = 0;
164 | GCC_PREPROCESSOR_DEFINITIONS = (
165 | "DEBUG=1",
166 | "$(inherited)",
167 | );
168 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
169 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
170 | GCC_WARN_UNDECLARED_SELECTOR = YES;
171 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
172 | GCC_WARN_UNUSED_FUNCTION = YES;
173 | GCC_WARN_UNUSED_VARIABLE = YES;
174 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
175 | MTL_ENABLE_DEBUG_INFO = YES;
176 | ONLY_ACTIVE_ARCH = YES;
177 | SDKROOT = iphoneos;
178 | };
179 | name = Debug;
180 | };
181 | DA08C4B21DAF39C600714E34 /* Release */ = {
182 | isa = XCBuildConfiguration;
183 | buildSettings = {
184 | ALWAYS_SEARCH_USER_PATHS = NO;
185 | CLANG_ANALYZER_NONNULL = YES;
186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
187 | CLANG_CXX_LIBRARY = "libc++";
188 | CLANG_ENABLE_MODULES = YES;
189 | CLANG_ENABLE_OBJC_ARC = YES;
190 | CLANG_WARN_BOOL_CONVERSION = YES;
191 | CLANG_WARN_CONSTANT_CONVERSION = YES;
192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
193 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
194 | CLANG_WARN_EMPTY_BODY = YES;
195 | CLANG_WARN_ENUM_CONVERSION = YES;
196 | CLANG_WARN_INFINITE_RECURSION = YES;
197 | CLANG_WARN_INT_CONVERSION = YES;
198 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
199 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
200 | CLANG_WARN_UNREACHABLE_CODE = YES;
201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
202 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
205 | ENABLE_NS_ASSERTIONS = NO;
206 | ENABLE_STRICT_OBJC_MSGSEND = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu99;
208 | GCC_NO_COMMON_BLOCKS = YES;
209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
211 | GCC_WARN_UNDECLARED_SELECTOR = YES;
212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
213 | GCC_WARN_UNUSED_FUNCTION = YES;
214 | GCC_WARN_UNUSED_VARIABLE = YES;
215 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
216 | MTL_ENABLE_DEBUG_INFO = NO;
217 | SDKROOT = iphoneos;
218 | VALIDATE_PRODUCT = YES;
219 | };
220 | name = Release;
221 | };
222 | DA08C4B41DAF39C600714E34 /* Debug */ = {
223 | isa = XCBuildConfiguration;
224 | buildSettings = {
225 | HEADER_SEARCH_PATHS = (
226 | "$(inherited)",
227 | "$(SRCROOT)/node_modules/react-native/React/**",
228 | "$(SRCROOT)/../react-native/React/**",
229 | );
230 | OTHER_LDFLAGS = "-ObjC";
231 | PRODUCT_NAME = "$(TARGET_NAME)";
232 | SKIP_INSTALL = YES;
233 | };
234 | name = Debug;
235 | };
236 | DA08C4B51DAF39C600714E34 /* Release */ = {
237 | isa = XCBuildConfiguration;
238 | buildSettings = {
239 | HEADER_SEARCH_PATHS = (
240 | "$(inherited)",
241 | "$(SRCROOT)/node_modules/react-native/React/**",
242 | "$(SRCROOT)/../react-native/React/**",
243 | );
244 | OTHER_LDFLAGS = "-ObjC";
245 | PRODUCT_NAME = "$(TARGET_NAME)";
246 | SKIP_INSTALL = YES;
247 | };
248 | name = Release;
249 | };
250 | /* End XCBuildConfiguration section */
251 |
252 | /* Begin XCConfigurationList section */
253 | DA08C4A51DAF39C600714E34 /* Build configuration list for PBXProject "RNPinch" */ = {
254 | isa = XCConfigurationList;
255 | buildConfigurations = (
256 | DA08C4B11DAF39C600714E34 /* Debug */,
257 | DA08C4B21DAF39C600714E34 /* Release */,
258 | );
259 | defaultConfigurationIsVisible = 0;
260 | defaultConfigurationName = Release;
261 | };
262 | DA08C4B31DAF39C600714E34 /* Build configuration list for PBXNativeTarget "RNPinch" */ = {
263 | isa = XCConfigurationList;
264 | buildConfigurations = (
265 | DA08C4B41DAF39C600714E34 /* Debug */,
266 | DA08C4B51DAF39C600714E34 /* Release */,
267 | );
268 | defaultConfigurationIsVisible = 0;
269 | defaultConfigurationName = Release;
270 | };
271 | /* End XCConfigurationList section */
272 | };
273 | rootObject = DA08C4A21DAF39C600714E34 /* Project object */;
274 | }
275 |
--------------------------------------------------------------------------------