├── LICENSE ├── README.md ├── package.json ├── plugin.xml ├── scripts └── realtime_cordovapush_icons.js ├── src ├── android │ ├── GcmReceiver.java │ ├── OrtcPushHandlerActivity.java │ ├── OrtcPushPlugin.java │ └── build.gradle └── ios │ ├── AppDelegate+RealtimeCordova.h │ ├── AppDelegate+RealtimeCordova.m │ ├── OrtcPushPlugin.h │ ├── OrtcPushPlugin.m │ ├── SocketRocket │ ├── SRWebSocket.h │ └── SRWebSocket.m │ └── ortc │ ├── Balancer.h │ ├── Balancer.m │ ├── OrtcClient.h │ └── OrtcClient.m └── www └── OrtcPlugin.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Realtime Framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordova Push Notifications for iOS/Android (using Realtime Messaging) 2 | 3 | ## Description 4 | 5 | This Cordova plugin should be used with the iOS/Android platform together with the Realtime Messaging library (ORTC) for Push Notifications support. 6 | 7 | ### Important 8 | 9 | Push Notifications only work in real devices for the iOS platform (not on simulator). 10 | 11 | * * * 12 | 13 | ## Step by step configuration iOS/Android 14 | 15 | 1. **Add the plugin using the Cordova CLI** 16 | 17 | cordova plugin add cordovapush 18 | 19 | 2. **Generate the APNS certificates for push notification on the Apple Developer Center** 20 | 21 | [Follow the iOS Push Notifications tutorial](http://messaging-public.realtime.co/documentation/starting-guide/mobilePushAPNS.html) 22 | 3. **Generate the Android API keys for push notification on the Google Cloud Messaging** 23 | 24 | [Follow the Android Push Notifications tutorial](http://messaging-public.realtime.co/documentation/starting-guide/mobilePushGCM.html) 25 | 26 | 4. **Build and run the app in an iOS/Android device** 27 | 28 | NOTE: iOS - The Push Notifications won't work on the simulators, only on actual devices. 29 | NOTE: Android - The Push Notifications will work on the emulators only if is a Google APIs emulator. 30 | 31 | * * * 32 | 33 | ## API reference 34 | 35 | ### Plugin 36 | 37 | * * * 38 | 39 | * **var promise = checkForNotifications()** 40 | 41 | This method is used to verify push notifications on buffer on the native code from the javascript interface. 42 | 43 | * **var promise = removeNotifications()** 44 | 45 | This method is used clear the last push notifications on buffer on the native code from the javascript interface. 46 | 47 | * promise - resolved after iOS/Android native finishes processing. 48 | * **var promise = connect(config)** 49 | 50 | This method is used to establish the ORTC connection. 51 | 52 | * config - is a JSONObject with the config to connect. Ex: {'appkey':'YOUR_APPLICATION_KEY', 'token':'myToken', 'metadata':'myMetadata', 'url':'https://ortc-developers.realtime.co/server/ssl/2.1/','projectId':'YOUR_GOOGLE_PROJECT_NUMBER'}. ProjectId only necessary on Android Push Notifications. 53 | * promise - resolve is call when connection is established. 54 | * **var promise = disconnect()** 55 | 56 | This method is used to disconnect the ORTC connection. 57 | * promise - resolved after connection is disconnected. 58 | 59 | * **var promise = getIsConnected()** 60 | 61 | Gets ortc client connection state. 62 | 63 | Resolves promise with parameter state 0 if not connected and 1 connected. 64 | * **var promise = subscribe(channel)** 65 | 66 | Subscribe a channel. Note: In order to receive the push notifications on the channel you just subscribed you have to add an event listener with the name "push-notification" to your html like: 67 | 68 | document.addEventListener("push-notification", 69 | function(notification) 70 | { 71 | window.plugins.OrtcPushPlugin.log("Push-Received channel: " + notification.channel + " payload: " + notification.payload); 72 | var payload = document.getElementById('payload'); 73 | payload.innerHTML = JSON.stringify( notification.payload ); 74 | payload.value = JSON.stringify( notification.payload ); 75 | }, false); 76 | 77 | **The object notification passed as argument** on the listener function is an JSONObject with the fields **channel**, **payload** and **tapped**, where channel is the name of the channel, payload is the content you sent (like a JSONObject or a String) and tapped is 0 or 1 indicating if the user tapped the notification. 78 | 79 | { 80 | channel: "mychannel", 81 | payload: {"sound":"default","badge":"1","name":"Joe","age" :"48"}, 82 | tapped: 1 83 | } 84 | 85 | **NOTE: Please in your payload try to avoid '\n' since you can have some issues.** 86 | 87 | * is a JSONObject with the channel to subscribe. Ex: {'channel':'mychannel'} 88 | * promise - resolved when channel is subscribed. 89 | * **var promise = unsubscribe(channel)** 90 | 91 | This method is used to unsubscribe a channel previously subscribed. 92 | 93 | * channel - is a JSONObject with the channel name to unsubscribe. Ex: {'channel':'mychannel'} 94 | * promise - resolved when channel is unsubscribed. 95 | * **var promise = setApplicationIconBadgeNumber(badge)** 96 | 97 | This method is used to set the application badge number on iOS. Not implemented on Android. 98 | 99 | * badge - the number to appear on the bage. 100 | * promise - resolved after iOS/Android native code finishes. 101 | * **send(config)** 102 | 103 | This method is used to send a message to a channel. 104 | 105 | * config - is a JSONObject with the channel to send the message. Ex: {'channel':'mychannel','message':'mymessage'}. 106 | * **var promise = cancelAllLocalNotifications()** 107 | 108 | This method is used to clear notifications from notification center. 109 | 110 | * promise - resolved after iOS/Android native code finishes. 111 | * **log(logString)** 112 | 113 | This is a handy method to log data into XCODE/AndroidStudio console from the javascript code. 114 | 115 | * logString - is the string to be logged into the console. 116 | * **onException event** 117 | 118 | To get the exceptions from the underlying native ortc client you must create the event listener with the name "onException" on your html code like: 119 | 120 | document.addEventListener("onException", function(exception){ 121 | window.plugins.OrtcPushPlugin.log("onException: " + exception.description); 122 | }, false); 123 | 124 | ### Setting notification icons for the Android platform 125 | 126 | * To configure the notification large icon you must have an image on your app resources folder `resources/android/drawable/` named `large_notification_icon` 127 | 128 | * To configure the notification small icon (for the notifications bar) you must have an image on your app resources folder `resources/android/drawable/` named `small_notification_icon`. 129 | 130 | * IMPORANT: The small icon image must be created following the [Android design guidelines](https://material.google.com/style/icons.html#icons-system-icons), otherwise it will be rendered as a white square or won't be rendered at all in the notifications bar. 131 | 132 | * To configure notification background color you must create a file named `colors.xml` in `resources/android/values/`. In this file you must set a color resource named `notification_color`. 133 | 134 | #####Example: 135 | 136 | 137 | #ff0000 138 | 139 | 140 | ### Set notifications display mode 141 | 142 | Only available for android. [Check android documentation](https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Heads-up) 143 | 144 | **var promise = enableHeadsUpNotifications()** Use this method to set the notification display type to Heads-up. This method persists the set value, to disable the heads-up notifications you must call `disableHeadsUpNotifications`. 145 | 146 | **var promise = disableHeadsUpNotifications()** Use this method to set the default notifications display (only the small icon is shown in the notification bar) and disable Heads-up. 147 | 148 | ## Usage example 149 | 150 | * * * 151 | 152 | Add to your HTML page HEAD section: 153 | 154 | 163 | 164 | Add to your app: 165 | 166 | //Establish connection with ORTC server and subscribe the channel entered in the input text box on the HTML interface. 167 | 168 | function subscribe() 169 | { 170 | if(window.plugins && window.plugins.OrtcPushPlugin){ 171 | var OrtcPushPlugin = window.plugins.OrtcPushPlugin; 172 | OrtcPushPlugin.log("Connecting"); 173 | 174 | OrtcPushPlugin.connect({'appkey':'YOUR_APPLICATION_KEY', 'token':'myToken', 'metadata':'myMetadata', 'url':'https://ortc-developers.realtime.co/server/ssl/2.1/','projectId':'YOUR_GOOGLE_PROJECT_NUMBER'}) 175 | .then(function (){ 176 | OrtcPushPlugin.log("Connected: "); 177 | var channel = document.getElementById('channel'); 178 | OrtcPushPlugin.log("Trying to subscribe: " + channel.value); 179 | OrtcPushPlugin.subscribe({'channel':channel.value}) 180 | .then(function (){ 181 | var subcribed = document.getElementById('subscribed'); 182 | subcribed.innerHTML = "subscribed: " + channel.value; 183 | OrtcPushPlugin.log("subscribed: " + channel.value); 184 | //OrtcPushPlugin.disconnect(); 185 | }); 186 | }); 187 | } 188 | }; 189 | 190 | //Catch the push-notification event when a new notification is received (or tapped by the user) 191 | //Shows the extra property of push notification payload (can be customized using the Realtime Custom Push REST endpoint) 192 | 193 | document.addEventListener("push-notification", 194 | function(notification) 195 | { 196 | window.plugins.OrtcPushPlugin.log("Push-Received channel: " + notification.channel + " payload: " + notification.payload); 197 | var payload = document.getElementById('payload'); 198 | payload.innerHTML = "payload: " + notification.payload.name; 199 | payload.value = "payload: " + notification.payload.name; 200 | }, false); 201 | 202 | ## Testing the custom push notifications delivery 203 | 204 | To test your Push Notifications you need to go through the setup process (see the iOS Push Notifications Starting Guide) and use the Realtime REST API to send a custom push with the following POST to https://ortc-mobilepush.realtime.co/mp/publish 205 | 206 | { 207 | "applicationKey": "[INSERT YOUR APP KEY]", 208 | "privateKey": "[INSERT YOUR APP PRIVATE KEY]", 209 | "channel" : "[INSERT CHANNEL TO SEND PUSH]", 210 | "message" : "[INSERT ALERT TEXT]", 211 | "payload" : "{ 212 | \"sound\" : \"default\", 213 | \"badge\" : \"1\", 214 | \"name\" : \"Joe\", 215 | \"age\" : \"48\" 216 | }" 217 | } 218 | 219 | Have fun pushing! -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordovapush", 3 | "version": "0.2.2", 4 | "description": "This Cordova plugin should be used with the iOS/Android platforms together with the Realtime Messaging library (ORTC) for Push Notifications support.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/realtime-framework/CordovaPush.git" 9 | }, 10 | "keywords": [ 11 | "cordova-android", 12 | "cordova-ios", 13 | "ecosystem:cordova" 14 | ], 15 | "author": "Realtime", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/realtime-framework/CordovaPush/issues" 19 | }, 20 | "homepage": "https://github.com/realtime-framework/CordovaPush#readme" 21 | } 22 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cordovapush 7 | Reatime 8 | 9 | 10 | This Cordova plugin should be used with the iOS/Android platforms together with the Realtime Messaging library (ORTC) for Push Notifications support. 11 | 12 | 13 | MIT 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /scripts/realtime_cordovapush_icons.js: -------------------------------------------------------------------------------- 1 | var filestocopy = [{ 2 | "resources/android/drawable/small_notification_icon.png": "platforms/android/res/drawable-ldpi/small_notification_icon.png", 3 | "resources/android/drawable/large_notification_icon.png": "platforms/android/res/drawable-ldpi/large_notification_icon.png", 4 | "resources/android/values/colors.xml": "platforms/android/res/values/colors.xml" 5 | }]; 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | filestocopy.forEach(function(obj) { 11 | Object.keys(obj).forEach(function(key) { 12 | var val = obj[key]; 13 | var srcfile = key; 14 | var destfile = val; 15 | console.log("copying "+srcfile+" to "+destfile); 16 | var destdir = path.dirname(destfile); 17 | if (fs.existsSync(srcfile) && fs.existsSync(destdir)) { 18 | fs.createReadStream(srcfile).pipe( 19 | fs.createWriteStream(destfile)); 20 | } 21 | }); 22 | }); -------------------------------------------------------------------------------- /src/android/GcmReceiver.java: -------------------------------------------------------------------------------- 1 | package co.realtime.plugins.android.cordovapush; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.os.Bundle; 12 | import android.preference.PreferenceManager; 13 | import android.support.v4.app.NotificationCompat; 14 | import android.support.v4.content.ContextCompat; 15 | import android.util.Log; 16 | 17 | import java.util.Random; 18 | 19 | import ibt.ortc.extensibility.GcmOrtcBroadcastReceiver; 20 | 21 | public class GcmReceiver extends GcmOrtcBroadcastReceiver { 22 | 23 | private static final String TAG = "GcmReceiver"; 24 | 25 | public GcmReceiver() { 26 | } 27 | 28 | @Override 29 | public void onReceive(Context context, Intent intent) { 30 | // Extract the payload from the message 31 | Bundle extras = intent.getExtras(); 32 | if (extras != null) { 33 | // if we are in the foreground, just surface the payload, else post it to the statusbar 34 | if (OrtcPushPlugin.isInForeground()) { 35 | extras.putBoolean("foreground", true); 36 | OrtcPushPlugin.sendExtras(extras, 0); 37 | } else { 38 | extras.putBoolean("foreground", false); 39 | // Send a notification if there is a message 40 | if (extras.getString("M") != null && extras.getString("M").length() != 0) { 41 | createNotification(context, extras); 42 | } 43 | } 44 | } 45 | } 46 | 47 | public void createNotification(Context context, Bundle extras) 48 | { 49 | NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 50 | String appName = getAppName(context); 51 | 52 | Intent notificationIntent = new Intent(context, OrtcPushHandlerActivity.class); 53 | notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); 54 | notificationIntent.putExtra("pushBundle", extras); 55 | 56 | PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); 57 | 58 | int defaults = Notification.DEFAULT_ALL; 59 | 60 | if (extras.getString("defaults") != null) { 61 | try { 62 | defaults = Integer.parseInt(extras.getString("defaults")); 63 | } catch (NumberFormatException e) {} 64 | } 65 | 66 | String channel = extras.getString("C"); 67 | String message = extras.getString("message"); 68 | 69 | int largeIcon = getIcon(context, "large_notification_icon"); 70 | 71 | Bitmap appIcon = BitmapFactory.decodeResource(context.getResources(), largeIcon); 72 | 73 | int color = getResource(context, "notification_color", "color"); 74 | 75 | int smallIcon = getIcon(context, "small_notification_icon"); 76 | NotificationCompat.Builder mBuilder = 77 | new NotificationCompat.Builder(context) 78 | .setDefaults(defaults) 79 | .setPriority(getAppPriority(context)) 80 | .setLargeIcon(appIcon) 81 | .setSmallIcon(smallIcon) 82 | .setWhen(System.currentTimeMillis()) 83 | .setContentTitle(context.getString(context.getApplicationInfo().labelRes)) 84 | .setContentIntent(contentIntent) 85 | .setAutoCancel(true); 86 | 87 | 88 | if (color != 0){ 89 | mBuilder.setColor(ContextCompat.getColor(context, color)); 90 | } 91 | 92 | if (message != null) { 93 | mBuilder.setContentText(message); 94 | } else { 95 | mBuilder.setContentText(""); 96 | } 97 | 98 | int notId = 0; 99 | 100 | try { 101 | notId = new Random().nextInt(); 102 | } 103 | catch(NumberFormatException e) { 104 | Log.e(TAG, "Number format exception - Error parsing Notification ID: " + e.getMessage()); 105 | } 106 | catch(Exception e) { 107 | Log.e(TAG, "Number format exception - Error parsing Notification ID" + e.getMessage()); 108 | } 109 | 110 | mNotificationManager.notify(appName, notId, mBuilder.build()); 111 | } 112 | 113 | private static int getIcon(Context context, String name){ 114 | int imageResource; 115 | String uri = "drawable/" + name; 116 | imageResource = context.getResources().getIdentifier(uri, null, context.getPackageName()); 117 | 118 | if (imageResource == 0){ 119 | imageResource = context.getApplicationInfo().icon; 120 | } 121 | return imageResource; 122 | } 123 | 124 | private static int getResource(Context context, String name, String type){ 125 | int resource; 126 | resource = context.getResources().getIdentifier(name, type, context.getPackageName()); 127 | return resource; 128 | } 129 | 130 | private static String getAppName(Context context) 131 | { 132 | CharSequence appName = 133 | context 134 | .getPackageManager() 135 | .getApplicationLabel(context.getApplicationInfo()); 136 | 137 | return (String)appName; 138 | } 139 | 140 | public static int getAppPriority(Context context){ 141 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 142 | int val = preferences.getInt("APP_PRIORITY", 0); 143 | return val; 144 | } 145 | 146 | public static void setAppPriority(Context context, int priority){ 147 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); 148 | SharedPreferences.Editor editor = preferences.edit(); 149 | editor.putInt("APP_PRIORITY", priority); 150 | editor.apply(); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/android/OrtcPushHandlerActivity.java: -------------------------------------------------------------------------------- 1 | package co.realtime.plugins.android.cordovapush; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | 9 | public class OrtcPushHandlerActivity extends Activity{ 10 | private static String TAG = "OrtcPushHandlerActivity"; 11 | 12 | @Override 13 | public void onCreate(Bundle savedInstanceState) 14 | { 15 | super.onCreate(savedInstanceState); 16 | Log.v(TAG, "onCreate"); 17 | 18 | boolean isPushPluginActive = OrtcPushPlugin.isActive(); 19 | if (!isPushPluginActive) { 20 | forceMainActivityReload(); 21 | } 22 | 23 | processPushBundle(); 24 | 25 | finish(); 26 | } 27 | 28 | private void processPushBundle() 29 | { 30 | Bundle extras = getIntent().getExtras(); 31 | 32 | if (extras != null) { 33 | Bundle originalExtras = extras.getBundle("pushBundle"); 34 | 35 | originalExtras.putBoolean("foreground", false); 36 | OrtcPushPlugin.sendExtras(originalExtras, 1); 37 | } 38 | } 39 | 40 | private void forceMainActivityReload() 41 | { 42 | PackageManager pm = getPackageManager(); 43 | Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName()); 44 | startActivity(launchIntent); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/android/OrtcPushPlugin.java: -------------------------------------------------------------------------------- 1 | package co.realtime.plugins.android.cordovapush; 2 | 3 | import android.app.NotificationManager; 4 | import android.content.Context; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | 8 | import org.apache.cordova.CallbackContext; 9 | import org.apache.cordova.CordovaInterface; 10 | import org.apache.cordova.CordovaPlugin; 11 | import org.apache.cordova.CordovaWebView; 12 | import org.json.JSONArray; 13 | import org.json.JSONException; 14 | import org.json.JSONObject; 15 | 16 | import java.util.HashMap; 17 | 18 | import ibt.ortc.api.Ortc; 19 | import ibt.ortc.extensibility.OnConnected; 20 | import ibt.ortc.extensibility.OnDisconnected; 21 | import ibt.ortc.extensibility.OnException; 22 | import ibt.ortc.extensibility.OnMessage; 23 | import ibt.ortc.extensibility.OnReconnected; 24 | import ibt.ortc.extensibility.OnReconnecting; 25 | import ibt.ortc.extensibility.OnSubscribed; 26 | import ibt.ortc.extensibility.OnUnsubscribed; 27 | import ibt.ortc.extensibility.OrtcClient; 28 | import ibt.ortc.extensibility.OrtcFactory; 29 | 30 | public class OrtcPushPlugin extends CordovaPlugin { 31 | 32 | public static final String TAG = "ORTCPushPlugin"; 33 | public static final String ACTION_CHECK_NOTIFICATIONS = "checkForNotifications"; 34 | public static final String ACTION_REMOVE_NOTIFICATIONS = "removeNotifications"; 35 | public static final String ACTION_SET_ICON = "setApplicationIconBadgeNumber"; 36 | public static final String ACTION_LOG = "log"; 37 | public static final String ACTION_CONNECT = "connect"; 38 | public static final String ACTION_DISCONNECT = "disconnect"; 39 | public static final String ACTION_SUBSCRIBE = "subscribe"; 40 | public static final String ACTION_UNSUBSCRIBE = "unsubscribe"; 41 | public static final String ACTION_CANCEL_NOTIFICATIONS = "cancelAllLocalNotifications"; 42 | public static final String ACTION_SEND_MESSAGE = "send"; 43 | private static final String ACTION_ENABLE_HEADS_UP_NOTIFICATIONS = "enableHeadsUpNotifications"; 44 | private static final String ACTION_DISABLE_HEADS_UP_NOTIFICATIONS = "disableHeadsUpNotifications"; 45 | private static final String ACTION_GET_CONNECTION_STATE = "getIsConnected"; 46 | private OrtcClient client; 47 | private static CordovaWebView gWebView; 48 | private static Bundle gCachedExtras = null; 49 | private static boolean gForeground = false; 50 | private static CordovaInterface sCordova; 51 | private static HashMap commands = new HashMap(); 52 | @Override 53 | public void initialize(final CordovaInterface cordova, CordovaWebView webView) { 54 | super.initialize(cordova, webView); 55 | sCordova = cordova; 56 | gForeground = true; 57 | try { 58 | Ortc ortc = new Ortc(); 59 | 60 | OrtcFactory factory; 61 | 62 | factory = ortc.loadOrtcFactory("IbtRealtimeSJ"); 63 | 64 | client = factory.createClient(); 65 | 66 | client.setHeartbeatActive(true); 67 | 68 | 69 | client.onConnected = new OnConnected() { 70 | @Override 71 | public void run(OrtcClient ortcClient) { 72 | CallbackContext call = (CallbackContext)commands.get(ACTION_CONNECT); 73 | if (call != null) 74 | call.success(); 75 | } 76 | }; 77 | 78 | 79 | client.onDisconnected = new OnDisconnected() { 80 | @Override 81 | public void run(OrtcClient ortcClient) { 82 | CallbackContext call = (CallbackContext)commands.get(ACTION_DISCONNECT); 83 | if (call != null) 84 | call.success(); 85 | } 86 | }; 87 | 88 | client.onUnsubscribed = new OnUnsubscribed() { 89 | @Override 90 | public void run(OrtcClient ortcClient, String channel) { 91 | CallbackContext call = (CallbackContext)commands.get(ACTION_UNSUBSCRIBE); 92 | if (call != null) 93 | call.success(); 94 | } 95 | }; 96 | 97 | client.onSubscribed = new OnSubscribed() { 98 | @Override 99 | public void run(OrtcClient ortcClient, String s) { 100 | CallbackContext call = (CallbackContext)commands.get(ACTION_SUBSCRIBE); 101 | if (call != null) 102 | call.success(); 103 | } 104 | }; 105 | 106 | client.onReconnected = new OnReconnected(){ 107 | @Override 108 | public void run(OrtcClient sender) { 109 | Log.i(TAG, "Reconnected"); 110 | } 111 | }; 112 | client.onReconnecting = new OnReconnecting(){ 113 | @Override 114 | public void run(OrtcClient sender) { 115 | Log.i(TAG, "Reconnecting"); 116 | } 117 | }; 118 | 119 | client.onException = new OnException() { 120 | @Override 121 | public void run(OrtcClient ortcClient, Exception e) { 122 | onException(e.toString()); 123 | } 124 | }; 125 | 126 | } catch (Exception e) { 127 | Log.i(TAG,e.toString()); 128 | } 129 | 130 | } 131 | 132 | @Override 133 | public void onPause(boolean multitasking) { 134 | super.onPause(multitasking); 135 | gForeground = false; 136 | } 137 | 138 | @Override 139 | public void onResume(boolean multitasking) { 140 | super.onResume(multitasking); 141 | gForeground = true; 142 | } 143 | 144 | @Override 145 | public void onDestroy() { 146 | super.onDestroy(); 147 | gForeground = false; 148 | gWebView = null; 149 | } 150 | 151 | @Override 152 | public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { 153 | 154 | gWebView = this.webView; 155 | final JSONArray argss = args; 156 | try { 157 | if (ACTION_LOG.equals(action)) { 158 | Log.i(TAG,args.get(0).toString()); 159 | callbackContext.success(); 160 | return true; 161 | } 162 | else if(ACTION_GET_CONNECTION_STATE.equals(action)){ 163 | int isConnected = client.getIsConnected()? 1: 0; 164 | callbackContext.success(isConnected); 165 | return true; 166 | } 167 | else if(ACTION_CONNECT.equals(action)){ 168 | commands.put(ACTION_CONNECT, callbackContext); 169 | cordova.getThreadPool().execute(new Runnable() { 170 | @Override 171 | public void run() { 172 | JSONObject arg_object = null; 173 | try { 174 | arg_object = argss.getJSONObject(0); 175 | client.setClusterUrl(arg_object.getString("url")); 176 | client.setConnectionMetadata(arg_object.getString("metadata")); 177 | client.setGoogleProjectId(arg_object.getString("projectId")); 178 | client.setApplicationContext(cordova.getActivity().getApplicationContext()); 179 | client.connect(arg_object.getString("appkey"),arg_object.getString("token")); 180 | } catch (JSONException e) { 181 | e.printStackTrace(); 182 | } 183 | } 184 | }); 185 | return true; 186 | } 187 | else if(ACTION_DISCONNECT.equals(action)){ 188 | commands.put(ACTION_DISCONNECT, callbackContext); 189 | client.disconnect(); 190 | return true; 191 | } 192 | else if(ACTION_SUBSCRIBE.equals(action)) { 193 | commands.put(ACTION_SUBSCRIBE, callbackContext); 194 | cordova.getThreadPool().execute(new Runnable() { 195 | @Override 196 | public void run() { 197 | JSONObject arg_object = null; 198 | try { 199 | arg_object = argss.getJSONObject(0); 200 | String channel = arg_object.getString("channel"); 201 | client.subscribeWithNotifications(channel, true, new OnMessage() { 202 | @Override 203 | public void run(OrtcClient ortcClient, final String channel, final String message) { 204 | cordova.getActivity().runOnUiThread(new Runnable() { 205 | @Override 206 | public void run() { 207 | Log.i(TAG, "Received message:" + message + " from channel: " + channel); 208 | } 209 | }); 210 | } 211 | }); 212 | } catch (JSONException e) { 213 | e.printStackTrace(); 214 | } 215 | } 216 | }); 217 | return true; 218 | } 219 | else if(ACTION_UNSUBSCRIBE.equals(action)){ 220 | commands.put(ACTION_UNSUBSCRIBE, callbackContext); 221 | cordova.getThreadPool().execute(new Runnable() { 222 | @Override 223 | public void run() { 224 | JSONObject arg_object = null; 225 | try { 226 | arg_object = argss.getJSONObject(0); 227 | String channel = arg_object.getString("channel"); 228 | client.unsubscribe(channel); 229 | } catch (JSONException e) { 230 | e.printStackTrace(); 231 | } 232 | } 233 | }); 234 | return true; 235 | } 236 | else if(ACTION_CANCEL_NOTIFICATIONS.equals(action)){ 237 | commands.put(ACTION_CANCEL_NOTIFICATIONS, callbackContext); 238 | final NotificationManager notificationManager = (NotificationManager) this.cordova.getActivity().getSystemService(this.cordova.getActivity().NOTIFICATION_SERVICE); 239 | notificationManager.cancelAll(); 240 | callbackContext.success(); 241 | return true; 242 | } 243 | else if(ACTION_SEND_MESSAGE.equals(action)){ 244 | JSONObject arg_object = args.getJSONObject(0); 245 | String channel = arg_object.getString("channel"); 246 | String message = arg_object.getString("message"); 247 | client.send(channel,message); 248 | return true; 249 | } 250 | else if(ACTION_CHECK_NOTIFICATIONS.equals(action)){ 251 | if ( gCachedExtras != null) { 252 | Log.v(TAG, "sending cached extras"); 253 | sendExtras(gCachedExtras, 1); 254 | } 255 | callbackContext.success(); 256 | return true; 257 | } 258 | else if(ACTION_REMOVE_NOTIFICATIONS.equals(action)){ 259 | Log.v(TAG, "removing cached extras"); 260 | gCachedExtras = null; 261 | callbackContext.success(); 262 | return true; 263 | } 264 | else if(ACTION_SET_ICON.equals(action)){ 265 | Log.i(TAG,"Set icon badge number not implemented in Android"); 266 | callbackContext.success(); 267 | return true; 268 | } 269 | else if(ACTION_ENABLE_HEADS_UP_NOTIFICATIONS.equals(action)){ 270 | cordova.getThreadPool().execute(new Runnable() { 271 | @Override 272 | public void run() { 273 | Context context = cordova.getActivity().getApplicationContext(); 274 | GcmReceiver.setAppPriority(context, 1); 275 | } 276 | }); 277 | return true; 278 | } 279 | else if(ACTION_DISABLE_HEADS_UP_NOTIFICATIONS.equals(action)){ 280 | cordova.getThreadPool().execute(new Runnable() { 281 | @Override 282 | public void run() { 283 | Context context = cordova.getActivity().getApplicationContext(); 284 | GcmReceiver.setAppPriority(context, 0); 285 | } 286 | }); 287 | return true; 288 | } 289 | callbackContext.error("Invalid action"); 290 | return false; 291 | } catch(Exception e) { 292 | System.err.println("Exception: " + e.getMessage()); 293 | callbackContext.error(e.getMessage()); 294 | return false; 295 | } 296 | } 297 | 298 | public static boolean isInForeground() 299 | { 300 | return gForeground; 301 | } 302 | 303 | public static boolean isActive() 304 | { 305 | return gWebView != null; 306 | } 307 | 308 | 309 | private static void onException(String error){ 310 | final String exception = String.format("window.plugins.OrtcPushPlugin.onException('%s');", error); 311 | sCordova.getActivity().runOnUiThread(new Runnable() { 312 | @Override 313 | public void run() { 314 | gWebView.loadUrl("javascript:" + exception); 315 | } 316 | }); 317 | } 318 | 319 | 320 | public static void sendJavascript(JSONObject json, int tapped) { 321 | try { 322 | String send = ""; 323 | String channel = json.getString("channel"); 324 | json.remove("channel"); 325 | try { 326 | new JSONObject(json.getString("payload")); 327 | send = String.format("window.plugins.OrtcPushPlugin.receiveRemoteNotification('%s',%s, %d);",channel,json.getString("payload"), tapped); 328 | } catch (JSONException ex) { 329 | send = String.format("window.plugins.OrtcPushPlugin.receiveRemoteNotification('%s','%s', %d);",channel,json.getString("payload"), tapped); 330 | } 331 | 332 | 333 | Log.i(TAG, "sendJavascript: " + send); 334 | 335 | if (gWebView != null) { 336 | final String finalSend = send; 337 | sCordova.getActivity().runOnUiThread(new Runnable() { 338 | @Override 339 | public void run() { 340 | gWebView.loadUrl("javascript:" + finalSend); 341 | } 342 | }); 343 | 344 | } 345 | } catch (Exception e) { 346 | Log.e(TAG, "sendJavascript: JSON exception"); 347 | } 348 | 349 | } 350 | 351 | public static void sendExtras(Bundle extras, int tapped) 352 | { 353 | if (extras != null) { 354 | if (gWebView != null) { 355 | sendJavascript(convertBundleToJson(extras), tapped); 356 | } else { 357 | Log.v(TAG, "sendExtras: caching extras to send at a later time."); 358 | gCachedExtras = extras; 359 | } 360 | } 361 | } 362 | 363 | private static JSONObject convertBundleToJson(Bundle extras) 364 | { 365 | try 366 | { 367 | JSONObject json = new JSONObject(); 368 | 369 | if (extras.containsKey("P")){ 370 | json = new JSONObject(); 371 | json.put("payload",extras.getString("P")); 372 | } 373 | else{ 374 | String message = extras.getString("M"); 375 | String newMsg = message.substring(message.indexOf("_", message.indexOf("_") + 1)+1); 376 | json.put("payload",newMsg); 377 | } 378 | 379 | if (extras.containsKey("C")){ 380 | json.put("channel", extras.getString("C")); 381 | } 382 | //Iterator it = extras.keySet().iterator(); 383 | /*while (it.hasNext()) 384 | { 385 | String key = it.next(); 386 | Object value = extras.get(key); 387 | 388 | if (key.equals("foreground")) 389 | { 390 | json.put(key, extras.getBoolean("foreground")); 391 | } 392 | else if (key.equals("C")){ 393 | json.put("channel", extras.getString("C")); 394 | } 395 | if (key.equals("M")) { 396 | String message = extras.getString("M"); 397 | String newMsg = null; 398 | String [] parts = message.split("_"); 399 | if(parts.length > 1){ 400 | try { 401 | JSONObject jsonObject = new JSONObject(parts[parts.length-1]); 402 | newMsg = jsonObject.getString("message"); 403 | 404 | } catch (JSONException e) { 405 | e.printStackTrace(); 406 | } 407 | } 408 | else{ 409 | newMsg = message; 410 | } 411 | //json.put("message", newMsg); 412 | } 413 | else if (key.equals("P")){ 414 | json.put("payload",extras.getString("P")); 415 | *//*String payloadString = value.toString(); 416 | if (payloadString.startsWith("{")) { 417 | try { 418 | JSONObject payloadJsonObject = new JSONObject(payloadString); 419 | Iterator payloadIt = payloadJsonObject.keys(); 420 | while(payloadIt.hasNext()){ 421 | String keyPayload = (String) payloadIt.next(); 422 | Object valuePaylod = payloadJsonObject.get(keyPayload); 423 | json.put(keyPayload,valuePaylod); 424 | } 425 | } 426 | catch (Exception e) { 427 | json.put(key, value); 428 | } 429 | }*//* 430 | 431 | } 432 | 433 | 434 | }*/ 435 | 436 | Log.v(TAG, "extrasToJSON: " + json.toString()); 437 | 438 | return json; 439 | } 440 | catch( JSONException e) 441 | { 442 | Log.e(TAG, "extrasToJSON: JSON exception"); 443 | return null; 444 | } 445 | 446 | } 447 | 448 | } 449 | -------------------------------------------------------------------------------- /src/android/build.gradle: -------------------------------------------------------------------------------- 1 | repositories{ 2 | jcenter() 3 | flatDir{ 4 | dirs 'src/android/libs' 5 | } 6 | } 7 | 8 | dependencies { 9 | compile 'co.realtime:messaging-android:2.1.+' 10 | } -------------------------------------------------------------------------------- /src/ios/AppDelegate+RealtimeCordova.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+RealtimeCordova.h 3 | // CordovaPush 4 | // 5 | // Created by Joao Caixinha on 06/07/15. 6 | // 7 | // 8 | 9 | #import "AppDelegate.h" 10 | #import "OrtcClient.h" 11 | 12 | @interface AppDelegate (RealtimeCordova) 13 | 14 | @property(retain, nonatomic)CDVViewController *viewController; 15 | @property(retain, nonatomic)NSDictionary *pushInfo; 16 | 17 | - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions; 18 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; 19 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; 20 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /src/ios/AppDelegate+RealtimeCordova.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+RealtimeCordova.m 3 | // CordovaPush 4 | // 5 | // Created by Joao Caixinha on 06/07/15. 6 | // 7 | // 8 | 9 | #import "AppDelegate+RealtimeCordova.h" 10 | #import 11 | 12 | static char launchNotificationKey; 13 | 14 | 15 | @implementation AppDelegate (RealtimeCordova) 16 | @dynamic pushInfo; 17 | 18 | 19 | 20 | 21 | + (void)load 22 | { 23 | Method original, change; 24 | 25 | original = class_getInstanceMethod(self, @selector(init)); 26 | change = class_getInstanceMethod(self, @selector(change_init)); 27 | method_exchangeImplementations(original, change); 28 | } 29 | 30 | - (AppDelegate *)change_init 31 | { 32 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registForNotifications) 33 | name:@"UIApplicationDidFinishLaunchingNotification" object:nil]; 34 | 35 | [[NSNotificationCenter defaultCenter] addObserver:self 36 | selector:@selector(processException:) 37 | name:@"onException" 38 | object:nil]; 39 | return [self change_init]; 40 | } 41 | 42 | 43 | 44 | - (BOOL)registForNotifications 45 | { 46 | 47 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 48 | if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { 49 | UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound categories:nil]; 50 | [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; 51 | [[UIApplication sharedApplication] registerForRemoteNotifications]; 52 | } else { 53 | [[UIApplication sharedApplication] registerForRemoteNotificationTypes: UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge]; 54 | } 55 | #else 56 | [application registerForRemoteNotificationTypes: UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge]; 57 | #endif 58 | 59 | 60 | [[NSNotificationCenter defaultCenter] addObserver:self 61 | selector:@selector(handlePushNotifications) 62 | name:@"checkForNotifications" 63 | object:nil]; 64 | 65 | [[NSNotificationCenter defaultCenter] addObserver:self 66 | selector:@selector(removeStoredNotifications) 67 | name:@"removeNotifications" 68 | object:nil]; 69 | 70 | return YES; 71 | } 72 | 73 | 74 | 75 | - (void)application:(UIApplication *)application 76 | didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 77 | { 78 | NSString* newToken = [deviceToken description]; 79 | newToken = [newToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; 80 | newToken = [newToken stringByReplacingOccurrencesOfString:@" " withString:@""]; 81 | NSLog(@"\n\n - didRegisterForRemoteNotificationsWithDeviceToken:\n%@\n", deviceToken); 82 | 83 | [OrtcClient performSelector:@selector(setDEVICE_TOKEN:) withObject:[[NSString alloc] initWithString:newToken]]; 84 | } 85 | 86 | 87 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 88 | { 89 | [self application:application didReceiveRemoteNotification:userInfo]; 90 | } 91 | 92 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo 93 | { 94 | 95 | UIApplicationState appState = UIApplicationStateActive; 96 | if ([application respondsToSelector:@selector(applicationState)]) { 97 | appState = application.applicationState; 98 | } 99 | 100 | int state; 101 | if(application.applicationState == UIApplicationStateActive) { 102 | state = 0; 103 | }else if(application.applicationState == UIApplicationStateBackground){ 104 | state = 0; 105 | }else if(application.applicationState == UIApplicationStateInactive){ 106 | state = 1; 107 | } 108 | 109 | 110 | if (appState == UIApplicationStateActive) { 111 | [self processPush:userInfo tapped:state]; 112 | } else { 113 | //save it for later 114 | self.pushInfo = userInfo; 115 | } 116 | } 117 | 118 | - (void)processException:(NSNotification *)Notification 119 | { 120 | NSDictionary *userInfo = Notification.userInfo; 121 | NSString* error = [NSString stringWithFormat:@"window.plugins.OrtcPushPlugin.onException('%@');", [userInfo objectForKey:@"exception"]]; 122 | [((UIWebView*)self.viewController.webView) stringByEvaluatingJavaScriptFromString:error]; 123 | } 124 | 125 | - (void)processPush:(NSDictionary *)userInfo tapped:(int)tapped 126 | { 127 | NSMutableDictionary *pushInfo = [[NSMutableDictionary alloc] init]; 128 | 129 | if ([[[userInfo objectForKey:@"aps" ] objectForKey:@"alert"] isKindOfClass:[NSString class]]) { 130 | [self handleStd:pushInfo from:userInfo]; 131 | }else 132 | { 133 | [self handleCustom:pushInfo from:userInfo]; 134 | } 135 | 136 | 137 | NSString * jsCallBack; 138 | NSString *channel = [userInfo objectForKey:@"C"]; 139 | 140 | if ([[pushInfo objectForKey:@"payload"] isKindOfClass:[NSString class]]) { 141 | 142 | jsCallBack = [NSString stringWithFormat:@"window.plugins.OrtcPushPlugin.receiveRemoteNotification('%@','%@',%d);",channel, [pushInfo objectForKey:@"payload"], tapped]; 143 | [((UIWebView*)self.viewController.webView) stringByEvaluatingJavaScriptFromString:jsCallBack]; 144 | 145 | }else{ 146 | 147 | NSError *error; 148 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[pushInfo objectForKey:@"payload"] 149 | options:(NSJSONWritingOptions) (NSJSONWritingPrettyPrinted) 150 | error:&error]; 151 | 152 | NSString *jsonstring = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 153 | 154 | jsCallBack = [NSString stringWithFormat:@"window.plugins.OrtcPushPlugin.receiveRemoteNotification('%@',%@ ,%d);",channel, jsonstring, tapped]; 155 | 156 | [((UIWebView*)self.viewController.webView) stringByEvaluatingJavaScriptFromString:jsCallBack]; 157 | } 158 | } 159 | 160 | 161 | 162 | - (void)handleStd:(NSMutableDictionary*)pushInfo from:(NSDictionary*)userInfo 163 | { 164 | NSString* msg = [userInfo objectForKey:@"M"]; 165 | int num = 0; 166 | NSUInteger len = [msg length]; 167 | unichar buffer[len+1]; 168 | [msg getCharacters: buffer range: NSMakeRange(0, len)]; 169 | 170 | NSString *finalM; 171 | for (int i=0; i i + 1) { 175 | finalM = [msg substringFromIndex:i+1]; 176 | } 177 | } 178 | } 179 | 180 | NSError *error = nil; 181 | NSData *jsonData = [finalM dataUsingEncoding:NSUTF8StringEncoding]; 182 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; 183 | 184 | if (json != nil) { 185 | [pushInfo setObject:json forKey:@"payload"]; 186 | }else 187 | { 188 | [pushInfo setObject:finalM forKey:@"payload"]; 189 | } 190 | } 191 | 192 | 193 | - (void)handleCustom:(NSMutableDictionary*)pushInfo from:(NSDictionary*)userInfo 194 | { 195 | NSMutableDictionary *payload = [[NSMutableDictionary alloc] init]; 196 | for (NSString* key in [[userInfo objectForKey:@"aps"] allKeys]) { 197 | if (![key isEqualToString:@"sound"] && ![key isEqualToString:@"badge"] && ![key isEqualToString:@"alert"]) { 198 | [payload setObject:[[userInfo objectForKey:@"aps"] objectForKey:key] forKey:key]; 199 | } 200 | } 201 | [pushInfo setObject:payload forKey:@"payload"]; 202 | } 203 | 204 | 205 | - (void)handlePushNotifications 206 | { 207 | if (self.pushInfo != nil) { 208 | [self processPush:self.pushInfo tapped:1]; 209 | [self removeStoredNotifications]; 210 | } 211 | } 212 | 213 | - (void)removeStoredNotifications 214 | { 215 | self.pushInfo = nil; 216 | } 217 | 218 | 219 | - (void)application:(UIApplication *)application 220 | didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 221 | { 222 | 223 | } 224 | 225 | 226 | 227 | 228 | - (NSDictionary *)pushInfo 229 | { 230 | return objc_getAssociatedObject(self, &launchNotificationKey); 231 | } 232 | 233 | - (void)setPushInfo:(NSDictionary *)aDictionary 234 | { 235 | objc_setAssociatedObject(self, &launchNotificationKey, aDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 236 | } 237 | 238 | 239 | 240 | 241 | @end 242 | -------------------------------------------------------------------------------- /src/ios/OrtcPushPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // OrtcPlugin.h 3 | // babblr 4 | // 5 | // Created by Joao Caixinha on 12/09/14. 6 | // 7 | // 8 | 9 | #import 10 | #import "../../../CordovaLib/Classes/Public/CDV.h" 11 | #import "OrtcClient.h" 12 | 13 | @interface OrtcPushPlugin : CDVPlugin 14 | 15 | 16 | @property(retain, nonatomic)OrtcClient *ortc; 17 | @property(retain, nonatomic)id message; 18 | @property(retain, nonatomic)NSMutableDictionary *connectCommand; 19 | 20 | - (void)checkForNotifications:(CDVInvokedUrlCommand*)command; 21 | - (void)removeNotifications:(CDVInvokedUrlCommand*)command; 22 | - (void)connect:(CDVInvokedUrlCommand*)command; 23 | - (void)disconnect:(CDVInvokedUrlCommand*)command; 24 | - (void)getIsConnected:(CDVInvokedUrlCommand*)command; 25 | - (void)subscribe:(CDVInvokedUrlCommand*)command; 26 | - (void)unsubscribe:(CDVInvokedUrlCommand*)command; 27 | - (void)setApplicationIconBadgeNumber:(CDVInvokedUrlCommand*)command; 28 | - (void)cancelAllLocalNotifications:(CDVInvokedUrlCommand*)command; 29 | - (void)log:(CDVInvokedUrlCommand*)command; 30 | - (void)enableHeadsUpNotifications:(CDVInvokedUrlCommand*)command; 31 | - (void)disableHeadsUpNotifications:(CDVInvokedUrlCommand*)command; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /src/ios/OrtcPushPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // OrtcPlugin.m 3 | // babblr 4 | // 5 | // Created by Joao Caixinha on 12/09/14. 6 | // 7 | // 8 | 9 | #import "OrtcPushPlugin.h" 10 | 11 | @implementation OrtcPushPlugin 12 | 13 | - (void)connect:(CDVInvokedUrlCommand*)command{ 14 | _connectCommand = [[NSMutableDictionary alloc] init]; 15 | [_connectCommand setObject:command forKey:@"connect"]; 16 | NSDictionary* args = [command.arguments objectAtIndex:0]; 17 | 18 | NSString* appKey = [args objectForKey:@"appkey"]; 19 | NSString* token = [args objectForKey:@"token"]; 20 | NSString* metadata = [args objectForKey:@"metadata"]; 21 | NSString* serverUrl = [args objectForKey:@"url"]; 22 | 23 | if (!_ortc) { 24 | // Instantiate Messaging Client 25 | _ortc = [OrtcClient ortcClientWithConfig:self]; 26 | 27 | // Set connection properties 28 | [_ortc setConnectionMetadata:metadata]; 29 | [_ortc setClusterUrl:serverUrl]; 30 | } 31 | 32 | // Connect 33 | [_ortc connect:appKey authenticationToken:token]; 34 | } 35 | 36 | - (void)disconnect:(CDVInvokedUrlCommand*)command{ 37 | [self trowException:@"ORTC not connected" forCommand:command code:^(CDVInvokedUrlCommand *cmd) { 38 | [_connectCommand setObject:cmd forKey:@"disconnect"]; 39 | [_ortc disconnect]; 40 | }]; 41 | 42 | 43 | } 44 | 45 | - (void)getIsConnected:(CDVInvokedUrlCommand*)command{ 46 | if(_connectCommand == nil){ 47 | _connectCommand = [[NSMutableDictionary alloc] init]; 48 | } 49 | 50 | [_connectCommand setObject:command forKey:@"getIsConnected"]; 51 | CDVPluginResult* pluginResult = nil; 52 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(_ortc != nil?[_ortc isConnected]:0)]; 53 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[[_connectCommand objectForKey:@"getIsConnected"] callbackId]]; 54 | } 55 | 56 | 57 | - (void)trowException:(NSString*)exception forCommand:(CDVInvokedUrlCommand*)command code:(void (^)(CDVInvokedUrlCommand*))code{ 58 | if (!_ortc) { 59 | [[NSNotificationCenter defaultCenter] postNotificationName:@"onException" object:nil userInfo:@{@"exception":exception}]; 60 | return; 61 | } 62 | code(command); 63 | } 64 | 65 | - (void)onConnected:(OrtcClient *)ortc{ 66 | CDVPluginResult* pluginResult = nil; 67 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 68 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[[_connectCommand objectForKey:@"connect"] callbackId]]; 69 | } 70 | 71 | - (void)onDisconnected:(OrtcClient *)ortc{ 72 | CDVPluginResult* pluginResult = nil; 73 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 74 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[[_connectCommand objectForKey:@"disconnect"] callbackId]]; 75 | } 76 | 77 | - (void)onException:(OrtcClient *)ortc error:(NSError *)error{ 78 | [[NSNotificationCenter defaultCenter] postNotificationName:@"onException" object:nil userInfo:@{@"exception":error.localizedDescription}]; 79 | } 80 | 81 | - (void)onReconnected:(OrtcClient *)ortc{ 82 | 83 | } 84 | 85 | - (void)onReconnecting:(OrtcClient *)ortc{ 86 | 87 | } 88 | 89 | - (void)onSubscribed:(OrtcClient *)ortc channel:(NSString *)channel{ 90 | CDVPluginResult* pluginResult = nil; 91 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 92 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[[_connectCommand objectForKey:[NSString stringWithFormat:@"sub:%@",channel]] callbackId]]; 93 | } 94 | 95 | - (void)onUnsubscribed:(OrtcClient *)ortc channel:(NSString *)channel{ 96 | CDVPluginResult* pluginResult = nil; 97 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 98 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[[_connectCommand objectForKey:[NSString stringWithFormat:@"unsub:%@",channel]] callbackId]]; 99 | } 100 | 101 | 102 | - (void)subscribe:(CDVInvokedUrlCommand*)command{ 103 | [self trowException:@"ORTC not connected" forCommand:command code:^(CDVInvokedUrlCommand *cmd) { 104 | NSString* channel = [[cmd.arguments objectAtIndex:0] objectForKey:@"channel"]; 105 | [_connectCommand setObject:cmd forKey:[NSString stringWithFormat:@"sub:%@",channel]]; 106 | 107 | [_ortc subscribeWithNotifications:channel subscribeOnReconnected:YES onMessage:^(OrtcClient* ortc, NSString* channel, NSString* message) { 108 | 109 | //NSLog(@"%@", message); 110 | }]; 111 | }]; 112 | } 113 | 114 | - (void)unsubscribe:(CDVInvokedUrlCommand*)command{ 115 | [self trowException:@"ORTC not connected" forCommand:command code:^(CDVInvokedUrlCommand *cmd) { 116 | NSString* channel = [[cmd.arguments objectAtIndex:0] objectForKey:@"channel"]; 117 | [_connectCommand setObject:cmd forKey:[NSString stringWithFormat:@"unsub:%@",channel]]; 118 | [_ortc unsubscribe:channel]; 119 | }]; 120 | } 121 | 122 | - (void)setApplicationIconBadgeNumber:(CDVInvokedUrlCommand*)command{ 123 | 124 | NSInteger badge = [[command.arguments objectAtIndex:0] integerValue]; 125 | [[UIApplication sharedApplication] setApplicationIconBadgeNumber:badge]; 126 | CDVPluginResult* pluginResult = nil; 127 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 128 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[command callbackId]]; 129 | } 130 | 131 | - (void)send:(CDVInvokedUrlCommand*)command{ 132 | [self trowException:@"ORTC not connected" forCommand:command code:^(CDVInvokedUrlCommand *cmd) { 133 | NSDictionary* args = [cmd.arguments objectAtIndex:0]; 134 | 135 | NSString* channel = [args objectForKey:@"channel"]; 136 | NSString* channelMsg = [args objectForKey:@"message"]; 137 | 138 | [_ortc send:channel message:channelMsg]; 139 | }]; 140 | } 141 | 142 | - (void)cancelAllLocalNotifications:(CDVInvokedUrlCommand*)command{ 143 | [[UIApplication sharedApplication] setApplicationIconBadgeNumber: 0]; 144 | [[UIApplication sharedApplication] cancelAllLocalNotifications]; 145 | CDVPluginResult* pluginResult = nil; 146 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 147 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[command callbackId]]; 148 | } 149 | 150 | 151 | - (void)checkForNotifications:(CDVInvokedUrlCommand*)command{ 152 | [[NSNotificationCenter defaultCenter] postNotificationName:@"checkForNotifications" object:@"OrtcPushPlugin"]; 153 | CDVPluginResult* pluginResult = nil; 154 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 155 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[command callbackId]]; 156 | } 157 | 158 | - (void)removeNotifications:(CDVInvokedUrlCommand*)command 159 | { 160 | [[NSNotificationCenter defaultCenter] postNotificationName:@"removeNotifications" object:@"OrtcPushPlugin"]; 161 | CDVPluginResult* pluginResult = nil; 162 | pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; 163 | [self.commandDelegate sendPluginResult:pluginResult callbackId:[command callbackId]]; 164 | } 165 | 166 | - (void)log:(CDVInvokedUrlCommand*)command{ 167 | NSLog(@"OrtcPushPlugin: %@",[command.arguments objectAtIndex:0]); 168 | } 169 | 170 | - (void)enableHeadsUpNotifications:(CDVInvokedUrlCommand*)command{ 171 | 172 | } 173 | 174 | - (void)disableHeadsUpNotifications:(CDVInvokedUrlCommand*)command{ 175 | 176 | } 177 | 178 | @end 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/ios/SocketRocket/SRWebSocket.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2012 Square Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | #import 18 | #import 19 | 20 | typedef NS_ENUM(NSInteger, SRReadyState) { 21 | SR_CONNECTING = 0, 22 | SR_OPEN = 1, 23 | SR_CLOSING = 2, 24 | SR_CLOSED = 3, 25 | }; 26 | 27 | typedef enum SRStatusCode : NSInteger { 28 | SRStatusCodeNormal = 1000, 29 | SRStatusCodeGoingAway = 1001, 30 | SRStatusCodeProtocolError = 1002, 31 | SRStatusCodeUnhandledType = 1003, 32 | // 1004 reserved. 33 | SRStatusNoStatusReceived = 1005, 34 | // 1004-1006 reserved. 35 | SRStatusCodeInvalidUTF8 = 1007, 36 | SRStatusCodePolicyViolated = 1008, 37 | SRStatusCodeMessageTooBig = 1009, 38 | } SRStatusCode; 39 | 40 | @class SRWebSocket; 41 | 42 | extern NSString *const SRWebSocketErrorDomain; 43 | extern NSString *const SRHTTPResponseErrorKey; 44 | 45 | #pragma mark - SRWebSocketDelegate 46 | 47 | @protocol SRWebSocketDelegate; 48 | 49 | #pragma mark - SRWebSocket 50 | 51 | @interface SRWebSocket : NSObject 52 | 53 | @property (nonatomic, weak) id delegate; 54 | 55 | @property (nonatomic, readonly) SRReadyState readyState; 56 | @property (nonatomic, readonly, retain) NSURL *url; 57 | 58 | // This returns the negotiated protocol. 59 | // It will be nil until after the handshake completes. 60 | @property (nonatomic, readonly, copy) NSString *protocol; 61 | 62 | // Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. 63 | - (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; 64 | - (id)initWithURLRequest:(NSURLRequest *)request; 65 | 66 | // Some helper constructors. 67 | - (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; 68 | - (id)initWithURL:(NSURL *)url; 69 | 70 | // Delegate queue will be dispatch_main_queue by default. 71 | // You cannot set both OperationQueue and dispatch_queue. 72 | - (void)setDelegateOperationQueue:(NSOperationQueue*) queue; 73 | - (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; 74 | 75 | // By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. 76 | - (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; 77 | - (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; 78 | 79 | // SRWebSockets are intended for one-time-use only. Open should be called once and only once. 80 | - (void)open; 81 | 82 | - (void)close; 83 | - (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; 84 | 85 | // Send a UTF8 String or Data. 86 | - (void)send:(id)data; 87 | 88 | // Send Data (can be nil) in a ping message. 89 | - (void)sendPing:(NSData *)data; 90 | 91 | @end 92 | 93 | #pragma mark - SRWebSocketDelegate 94 | 95 | @protocol SRWebSocketDelegate 96 | 97 | // message will either be an NSString if the server is using text 98 | // or NSData if the server is using binary. 99 | - (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; 100 | 101 | @optional 102 | 103 | - (void)webSocketDidOpen:(SRWebSocket *)webSocket; 104 | - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; 105 | - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; 106 | - (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; 107 | 108 | @end 109 | 110 | #pragma mark - NSURLRequest (CertificateAdditions) 111 | 112 | @interface NSURLRequest (CertificateAdditions) 113 | 114 | @property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; 115 | 116 | @end 117 | 118 | #pragma mark - NSMutableURLRequest (CertificateAdditions) 119 | 120 | @interface NSMutableURLRequest (CertificateAdditions) 121 | 122 | @property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; 123 | 124 | @end 125 | 126 | #pragma mark - NSRunLoop (SRWebSocket) 127 | 128 | @interface NSRunLoop (SRWebSocket) 129 | 130 | + (NSRunLoop *)SR_networkRunLoop; 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /src/ios/SocketRocket/SRWebSocket.m: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2012 Square Inc. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | #import "SRWebSocket.h" 19 | 20 | #if TARGET_OS_IPHONE 21 | #define HAS_ICU 22 | #endif 23 | 24 | #ifdef HAS_ICU 25 | #import 26 | #endif 27 | 28 | #if TARGET_OS_IPHONE 29 | #import 30 | #else 31 | #import 32 | #endif 33 | 34 | #import 35 | #import 36 | 37 | #if OS_OBJECT_USE_OBJC_RETAIN_RELEASE 38 | #define sr_dispatch_retain(x) 39 | #define sr_dispatch_release(x) 40 | #define maybe_bridge(x) ((__bridge void *) x) 41 | #else 42 | #define sr_dispatch_retain(x) dispatch_retain(x) 43 | #define sr_dispatch_release(x) dispatch_release(x) 44 | #define maybe_bridge(x) (x) 45 | #endif 46 | 47 | #if !__has_feature(objc_arc) 48 | #error SocketRocket must be compiled with ARC enabled 49 | #endif 50 | 51 | 52 | typedef enum { 53 | SROpCodeTextFrame = 0x1, 54 | SROpCodeBinaryFrame = 0x2, 55 | // 3-7 reserved. 56 | SROpCodeConnectionClose = 0x8, 57 | SROpCodePing = 0x9, 58 | SROpCodePong = 0xA, 59 | // B-F reserved. 60 | } SROpCode; 61 | 62 | typedef struct { 63 | BOOL fin; 64 | // BOOL rsv1; 65 | // BOOL rsv2; 66 | // BOOL rsv3; 67 | uint8_t opcode; 68 | BOOL masked; 69 | uint64_t payload_length; 70 | } frame_header; 71 | 72 | static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 73 | 74 | static inline int32_t validate_dispatch_data_partial_string(NSData *data); 75 | static inline void SRFastLog(NSString *format, ...); 76 | 77 | @interface NSData (SRWebSocket) 78 | 79 | - (NSString *)stringBySHA1ThenBase64Encoding; 80 | 81 | @end 82 | 83 | 84 | @interface NSString (SRWebSocket) 85 | 86 | - (NSString *)stringBySHA1ThenBase64Encoding; 87 | 88 | @end 89 | 90 | 91 | @interface NSURL (SRWebSocket) 92 | 93 | // The origin isn't really applicable for a native application. 94 | // So instead, just map ws -> http and wss -> https. 95 | - (NSString *)SR_origin; 96 | 97 | @end 98 | 99 | 100 | @interface _SRRunLoopThread : NSThread 101 | 102 | @property (nonatomic, readonly) NSRunLoop *runLoop; 103 | 104 | @end 105 | 106 | 107 | static NSString *newSHA1String(const char *bytes, size_t length) { 108 | uint8_t md[CC_SHA1_DIGEST_LENGTH]; 109 | 110 | assert(length >= 0); 111 | assert(length <= UINT32_MAX); 112 | CC_SHA1(bytes, (CC_LONG)length, md); 113 | 114 | NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; 115 | 116 | if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { 117 | return [data base64EncodedStringWithOptions:0]; 118 | } 119 | 120 | #pragma clang diagnostic push 121 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 122 | return [data base64Encoding]; 123 | #pragma clang diagnostic pop 124 | } 125 | 126 | @implementation NSData (SRWebSocket) 127 | 128 | - (NSString *)stringBySHA1ThenBase64Encoding; 129 | { 130 | return newSHA1String(self.bytes, self.length); 131 | } 132 | 133 | @end 134 | 135 | 136 | @implementation NSString (SRWebSocket) 137 | 138 | - (NSString *)stringBySHA1ThenBase64Encoding; 139 | { 140 | return newSHA1String(self.UTF8String, self.length); 141 | } 142 | 143 | @end 144 | 145 | NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; 146 | NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; 147 | 148 | // Returns number of bytes consumed. Returning 0 means you didn't match. 149 | // Sends bytes to callback handler; 150 | typedef size_t (^stream_scanner)(NSData *collected_data); 151 | 152 | typedef void (^data_callback)(SRWebSocket *webSocket, NSData *data); 153 | 154 | @interface SRIOConsumer : NSObject { 155 | stream_scanner _scanner; 156 | data_callback _handler; 157 | size_t _bytesNeeded; 158 | BOOL _readToCurrentFrame; 159 | BOOL _unmaskBytes; 160 | } 161 | @property (nonatomic, copy, readonly) stream_scanner consumer; 162 | @property (nonatomic, copy, readonly) data_callback handler; 163 | @property (nonatomic, assign) size_t bytesNeeded; 164 | @property (nonatomic, assign, readonly) BOOL readToCurrentFrame; 165 | @property (nonatomic, assign, readonly) BOOL unmaskBytes; 166 | 167 | @end 168 | 169 | // This class is not thread-safe, and is expected to always be run on the same queue. 170 | @interface SRIOConsumerPool : NSObject 171 | 172 | - (id)initWithBufferCapacity:(NSUInteger)poolSize; 173 | 174 | - (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; 175 | - (void)returnConsumer:(SRIOConsumer *)consumer; 176 | 177 | @end 178 | 179 | @interface SRWebSocket () 180 | 181 | - (void)_writeData:(NSData *)data; 182 | - (void)_closeWithProtocolError:(NSString *)message; 183 | - (void)_failWithError:(NSError *)error; 184 | 185 | - (void)_disconnect; 186 | 187 | - (void)_readFrameNew; 188 | - (void)_readFrameContinue; 189 | 190 | - (void)_pumpScanner; 191 | 192 | - (void)_pumpWriting; 193 | 194 | - (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; 195 | - (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; 196 | - (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; 197 | - (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; 198 | - (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; 199 | 200 | - (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; 201 | 202 | - (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; 203 | - (void)_SR_commonInit; 204 | 205 | - (void)_initializeStreams; 206 | - (void)_connect; 207 | 208 | @property (nonatomic) SRReadyState readyState; 209 | 210 | @property (nonatomic) NSOperationQueue *delegateOperationQueue; 211 | @property (nonatomic) dispatch_queue_t delegateDispatchQueue; 212 | 213 | @end 214 | 215 | 216 | @implementation SRWebSocket { 217 | NSInteger _webSocketVersion; 218 | 219 | NSOperationQueue *_delegateOperationQueue; 220 | dispatch_queue_t _delegateDispatchQueue; 221 | 222 | dispatch_queue_t _workQueue; 223 | NSMutableArray *_consumers; 224 | 225 | NSInputStream *_inputStream; 226 | NSOutputStream *_outputStream; 227 | 228 | NSMutableData *_readBuffer; 229 | NSUInteger _readBufferOffset; 230 | 231 | NSMutableData *_outputBuffer; 232 | NSUInteger _outputBufferOffset; 233 | 234 | uint8_t _currentFrameOpcode; 235 | size_t _currentFrameCount; 236 | size_t _readOpCount; 237 | uint32_t _currentStringScanPosition; 238 | NSMutableData *_currentFrameData; 239 | 240 | NSString *_closeReason; 241 | 242 | NSString *_secKey; 243 | 244 | BOOL _pinnedCertFound; 245 | 246 | uint8_t _currentReadMaskKey[4]; 247 | size_t _currentReadMaskOffset; 248 | 249 | BOOL _consumerStopped; 250 | 251 | BOOL _closeWhenFinishedWriting; 252 | BOOL _failed; 253 | 254 | BOOL _secure; 255 | NSURLRequest *_urlRequest; 256 | 257 | CFHTTPMessageRef _receivedHTTPHeaders; 258 | 259 | BOOL _sentClose; 260 | BOOL _didFail; 261 | int _closeCode; 262 | 263 | BOOL _isPumping; 264 | 265 | NSMutableSet *_scheduledRunloops; 266 | 267 | // We use this to retain ourselves. 268 | __strong SRWebSocket *_selfRetain; 269 | 270 | NSArray *_requestedProtocols; 271 | SRIOConsumerPool *_consumerPool; 272 | } 273 | 274 | @synthesize delegate = _delegate; 275 | @synthesize url = _url; 276 | @synthesize readyState = _readyState; 277 | @synthesize protocol = _protocol; 278 | 279 | static __strong NSData *CRLFCRLF; 280 | 281 | + (void)initialize; 282 | { 283 | CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; 284 | } 285 | 286 | - (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; 287 | { 288 | self = [super init]; 289 | if (self) { 290 | assert(request.URL); 291 | _url = request.URL; 292 | _urlRequest = request; 293 | 294 | _requestedProtocols = [protocols copy]; 295 | 296 | [self _SR_commonInit]; 297 | } 298 | 299 | return self; 300 | } 301 | 302 | - (id)initWithURLRequest:(NSURLRequest *)request; 303 | { 304 | return [self initWithURLRequest:request protocols:nil]; 305 | } 306 | 307 | - (id)initWithURL:(NSURL *)url; 308 | { 309 | return [self initWithURL:url protocols:nil]; 310 | } 311 | 312 | - (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; 313 | { 314 | NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; 315 | return [self initWithURLRequest:request protocols:protocols]; 316 | } 317 | 318 | - (void)_SR_commonInit; 319 | { 320 | 321 | NSString *scheme = _url.scheme.lowercaseString; 322 | assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); 323 | 324 | if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { 325 | _secure = YES; 326 | } 327 | 328 | _readyState = SR_CONNECTING; 329 | _consumerStopped = YES; 330 | _webSocketVersion = 13; 331 | 332 | _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); 333 | 334 | // Going to set a specific on the queue so we can validate we're on the work queue 335 | dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL); 336 | 337 | _delegateDispatchQueue = dispatch_get_main_queue(); 338 | sr_dispatch_retain(_delegateDispatchQueue); 339 | 340 | _readBuffer = [[NSMutableData alloc] init]; 341 | _outputBuffer = [[NSMutableData alloc] init]; 342 | 343 | _currentFrameData = [[NSMutableData alloc] init]; 344 | 345 | _consumers = [[NSMutableArray alloc] init]; 346 | 347 | _consumerPool = [[SRIOConsumerPool alloc] init]; 348 | 349 | _scheduledRunloops = [[NSMutableSet alloc] init]; 350 | 351 | [self _initializeStreams]; 352 | 353 | // default handlers 354 | } 355 | 356 | - (void)assertOnWorkQueue; 357 | { 358 | assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); 359 | } 360 | 361 | - (void)dealloc 362 | { 363 | _inputStream.delegate = nil; 364 | _outputStream.delegate = nil; 365 | 366 | [_inputStream close]; 367 | [_outputStream close]; 368 | 369 | sr_dispatch_release(_workQueue); 370 | _workQueue = NULL; 371 | 372 | if (_receivedHTTPHeaders) { 373 | CFRelease(_receivedHTTPHeaders); 374 | _receivedHTTPHeaders = NULL; 375 | } 376 | 377 | if (_delegateDispatchQueue) { 378 | sr_dispatch_release(_delegateDispatchQueue); 379 | _delegateDispatchQueue = NULL; 380 | } 381 | } 382 | 383 | #ifndef NDEBUG 384 | 385 | - (void)setReadyState:(SRReadyState)aReadyState; 386 | { 387 | [self willChangeValueForKey:@"readyState"]; 388 | assert(aReadyState > _readyState); 389 | _readyState = aReadyState; 390 | [self didChangeValueForKey:@"readyState"]; 391 | } 392 | 393 | #endif 394 | 395 | - (void)open; 396 | { 397 | assert(_url); 398 | NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); 399 | 400 | _selfRetain = self; 401 | 402 | [self _connect]; 403 | } 404 | 405 | // Calls block on delegate queue 406 | - (void)_performDelegateBlock:(dispatch_block_t)block; 407 | { 408 | if (_delegateOperationQueue) { 409 | [_delegateOperationQueue addOperationWithBlock:block]; 410 | } else { 411 | assert(_delegateDispatchQueue); 412 | dispatch_async(_delegateDispatchQueue, block); 413 | } 414 | } 415 | 416 | - (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; 417 | { 418 | if (queue) { 419 | sr_dispatch_retain(queue); 420 | } 421 | 422 | if (_delegateDispatchQueue) { 423 | sr_dispatch_release(_delegateDispatchQueue); 424 | } 425 | 426 | _delegateDispatchQueue = queue; 427 | } 428 | 429 | - (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; 430 | { 431 | NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); 432 | 433 | if (acceptHeader == nil) { 434 | return NO; 435 | } 436 | 437 | NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; 438 | NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; 439 | 440 | return [acceptHeader isEqualToString:expectedAccept]; 441 | } 442 | 443 | - (void)_HTTPHeadersDidFinish; 444 | { 445 | NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); 446 | 447 | if (responseCode >= 400) { 448 | SRFastLog(@"Request failed with response code %d", responseCode); 449 | [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; 450 | return; 451 | } 452 | 453 | if(![self _checkHandshake:_receivedHTTPHeaders]) { 454 | [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; 455 | return; 456 | } 457 | 458 | NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); 459 | if (negotiatedProtocol) { 460 | // Make sure we requested the protocol 461 | if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { 462 | [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; 463 | return; 464 | } 465 | 466 | _protocol = negotiatedProtocol; 467 | } 468 | 469 | self.readyState = SR_OPEN; 470 | 471 | if (!_didFail) { 472 | [self _readFrameNew]; 473 | } 474 | 475 | [self _performDelegateBlock:^{ 476 | if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { 477 | [self.delegate webSocketDidOpen:self]; 478 | }; 479 | }]; 480 | } 481 | 482 | 483 | - (void)_readHTTPHeader; 484 | { 485 | if (_receivedHTTPHeaders == NULL) { 486 | _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); 487 | } 488 | 489 | [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { 490 | CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); 491 | 492 | if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { 493 | SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); 494 | [self _HTTPHeadersDidFinish]; 495 | } else { 496 | [self _readHTTPHeader]; 497 | } 498 | }]; 499 | } 500 | 501 | - (void)didConnect 502 | { 503 | SRFastLog(@"Connected"); 504 | CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); 505 | 506 | // Set host first so it defaults 507 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); 508 | 509 | NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; 510 | SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); 511 | 512 | if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { 513 | _secKey = [keyBytes base64EncodedStringWithOptions:0]; 514 | } else { 515 | #pragma clang diagnostic push 516 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 517 | _secKey = [keyBytes base64Encoding]; 518 | #pragma clang diagnostic pop 519 | } 520 | 521 | assert([_secKey length] == 24); 522 | 523 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); 524 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); 525 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); 526 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); 527 | 528 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); 529 | 530 | if (_requestedProtocols) { 531 | CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); 532 | } 533 | 534 | [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 535 | CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); 536 | }]; 537 | 538 | NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); 539 | 540 | CFRelease(request); 541 | 542 | [self _writeData:message]; 543 | [self _readHTTPHeader]; 544 | } 545 | 546 | - (void)_initializeStreams; 547 | { 548 | assert(_url.port.unsignedIntValue <= UINT32_MAX); 549 | uint32_t port = _url.port.unsignedIntValue; 550 | if (port == 0) { 551 | if (!_secure) { 552 | port = 80; 553 | } else { 554 | port = 443; 555 | } 556 | } 557 | NSString *host = _url.host; 558 | 559 | CFReadStreamRef readStream = NULL; 560 | CFWriteStreamRef writeStream = NULL; 561 | 562 | CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); 563 | 564 | _outputStream = CFBridgingRelease(writeStream); 565 | _inputStream = CFBridgingRelease(readStream); 566 | 567 | 568 | if (_secure) { 569 | NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; 570 | 571 | [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; 572 | 573 | // If we're using pinned certs, don't validate the certificate chain 574 | if ([_urlRequest SR_SSLPinnedCertificates].count) { 575 | [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; 576 | } 577 | 578 | #if DEBUG 579 | [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; 580 | NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); 581 | #endif 582 | 583 | [_outputStream setProperty:SSLOptions 584 | forKey:(__bridge id)kCFStreamPropertySSLSettings]; 585 | } 586 | 587 | _inputStream.delegate = self; 588 | _outputStream.delegate = self; 589 | } 590 | 591 | - (void)_connect; 592 | { 593 | if (!_scheduledRunloops.count) { 594 | [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; 595 | } 596 | 597 | 598 | [_outputStream open]; 599 | [_inputStream open]; 600 | } 601 | 602 | - (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; 603 | { 604 | [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; 605 | [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; 606 | 607 | [_scheduledRunloops addObject:@[aRunLoop, mode]]; 608 | } 609 | 610 | - (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; 611 | { 612 | [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; 613 | [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; 614 | 615 | [_scheduledRunloops removeObject:@[aRunLoop, mode]]; 616 | } 617 | 618 | - (void)close; 619 | { 620 | [self closeWithCode:SRStatusCodeNormal reason:nil]; 621 | } 622 | 623 | - (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; 624 | { 625 | assert(code); 626 | dispatch_async(_workQueue, ^{ 627 | if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) { 628 | return; 629 | } 630 | 631 | BOOL wasConnecting = self.readyState == SR_CONNECTING; 632 | 633 | self.readyState = SR_CLOSING; 634 | 635 | SRFastLog(@"Closing with code %d reason %@", code, reason); 636 | 637 | if (wasConnecting) { 638 | [self _disconnect]; 639 | return; 640 | } 641 | 642 | size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; 643 | NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; 644 | NSData *payload = mutablePayload; 645 | 646 | ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code); 647 | 648 | if (reason) { 649 | NSRange remainingRange = {0}; 650 | 651 | NSUInteger usedLength = 0; 652 | 653 | BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; 654 | #pragma unused (success) 655 | 656 | assert(success); 657 | assert(remainingRange.length == 0); 658 | 659 | if (usedLength != maxMsgSize) { 660 | payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; 661 | } 662 | } 663 | 664 | 665 | [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload]; 666 | }); 667 | } 668 | 669 | - (void)_closeWithProtocolError:(NSString *)message; 670 | { 671 | // Need to shunt this on the _callbackQueue first to see if they received any messages 672 | [self _performDelegateBlock:^{ 673 | [self closeWithCode:SRStatusCodeProtocolError reason:message]; 674 | dispatch_async(_workQueue, ^{ 675 | [self _disconnect]; 676 | }); 677 | }]; 678 | } 679 | 680 | - (void)_failWithError:(NSError *)error; 681 | { 682 | dispatch_async(_workQueue, ^{ 683 | if (self.readyState != SR_CLOSED) { 684 | _failed = YES; 685 | [self _performDelegateBlock:^{ 686 | if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { 687 | [self.delegate webSocket:self didFailWithError:error]; 688 | } 689 | }]; 690 | 691 | self.readyState = SR_CLOSED; 692 | _selfRetain = nil; 693 | 694 | SRFastLog(@"Failing with error %@", error.localizedDescription); 695 | 696 | [self _disconnect]; 697 | } 698 | }); 699 | } 700 | 701 | - (void)_writeData:(NSData *)data; 702 | { 703 | [self assertOnWorkQueue]; 704 | 705 | if (_closeWhenFinishedWriting) { 706 | return; 707 | } 708 | [_outputBuffer appendData:data]; 709 | [self _pumpWriting]; 710 | } 711 | 712 | - (void)send:(id)data; 713 | { 714 | NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); 715 | // TODO: maybe not copy this for performance 716 | data = [data copy]; 717 | dispatch_async(_workQueue, ^{ 718 | if ([data isKindOfClass:[NSString class]]) { 719 | [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; 720 | } else if ([data isKindOfClass:[NSData class]]) { 721 | [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; 722 | } else if (data == nil) { 723 | [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; 724 | } else { 725 | assert(NO); 726 | } 727 | }); 728 | } 729 | 730 | - (void)sendPing:(NSData *)data; 731 | { 732 | NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); 733 | // TODO: maybe not copy this for performance 734 | data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty 735 | dispatch_async(_workQueue, ^{ 736 | [self _sendFrameWithOpcode:SROpCodePing data:data]; 737 | }); 738 | } 739 | 740 | - (void)handlePing:(NSData *)pingData; 741 | { 742 | // Need to pingpong this off _callbackQueue first to make sure messages happen in order 743 | [self _performDelegateBlock:^{ 744 | dispatch_async(_workQueue, ^{ 745 | [self _sendFrameWithOpcode:SROpCodePong data:pingData]; 746 | }); 747 | }]; 748 | } 749 | 750 | - (void)handlePong:(NSData *)pongData; 751 | { 752 | SRFastLog(@"Received pong"); 753 | [self _performDelegateBlock:^{ 754 | if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { 755 | [self.delegate webSocket:self didReceivePong:pongData]; 756 | } 757 | }]; 758 | } 759 | 760 | - (void)_handleMessage:(id)message 761 | { 762 | SRFastLog(@"Received message"); 763 | [self _performDelegateBlock:^{ 764 | [self.delegate webSocket:self didReceiveMessage:message]; 765 | }]; 766 | } 767 | 768 | 769 | static inline BOOL closeCodeIsValid(int closeCode) { 770 | if (closeCode < 1000) { 771 | return NO; 772 | } 773 | 774 | if (closeCode >= 1000 && closeCode <= 1011) { 775 | if (closeCode == 1004 || 776 | closeCode == 1005 || 777 | closeCode == 1006) { 778 | return NO; 779 | } 780 | return YES; 781 | } 782 | 783 | if (closeCode >= 3000 && closeCode <= 3999) { 784 | return YES; 785 | } 786 | 787 | if (closeCode >= 4000 && closeCode <= 4999) { 788 | return YES; 789 | } 790 | 791 | return NO; 792 | } 793 | 794 | // Note from RFC: 795 | // 796 | // If there is a body, the first two 797 | // bytes of the body MUST be a 2-byte unsigned integer (in network byte 798 | // order) representing a status code with value /code/ defined in 799 | // Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 800 | // encoded data with value /reason/, the interpretation of which is not 801 | // defined by this specification. 802 | 803 | - (void)handleCloseWithData:(NSData *)data; 804 | { 805 | size_t dataSize = data.length; 806 | __block uint16_t closeCode = 0; 807 | 808 | SRFastLog(@"Received close frame"); 809 | 810 | if (dataSize == 1) { 811 | // TODO handle error 812 | [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; 813 | return; 814 | } else if (dataSize >= 2) { 815 | [data getBytes:&closeCode length:sizeof(closeCode)]; 816 | _closeCode = EndianU16_BtoN(closeCode); 817 | if (!closeCodeIsValid(_closeCode)) { 818 | [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; 819 | return; 820 | } 821 | if (dataSize > 2) { 822 | _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; 823 | if (!_closeReason) { 824 | [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; 825 | return; 826 | } 827 | } 828 | } else { 829 | _closeCode = SRStatusNoStatusReceived; 830 | } 831 | 832 | [self assertOnWorkQueue]; 833 | 834 | if (self.readyState == SR_OPEN) { 835 | [self closeWithCode:1000 reason:nil]; 836 | } 837 | dispatch_async(_workQueue, ^{ 838 | [self _disconnect]; 839 | }); 840 | } 841 | 842 | - (void)_disconnect; 843 | { 844 | [self assertOnWorkQueue]; 845 | SRFastLog(@"Trying to disconnect"); 846 | _closeWhenFinishedWriting = YES; 847 | [self _pumpWriting]; 848 | } 849 | 850 | - (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; 851 | { 852 | // Check that the current data is valid UTF8 853 | 854 | BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); 855 | if (!isControlFrame) { 856 | [self _readFrameNew]; 857 | } else { 858 | dispatch_async(_workQueue, ^{ 859 | [self _readFrameContinue]; 860 | }); 861 | } 862 | 863 | switch (opcode) { 864 | case SROpCodeTextFrame: { 865 | NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; 866 | if (str == nil && frameData) { 867 | [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; 868 | dispatch_async(_workQueue, ^{ 869 | [self _disconnect]; 870 | }); 871 | 872 | return; 873 | } 874 | [self _handleMessage:str]; 875 | break; 876 | } 877 | case SROpCodeBinaryFrame: 878 | [self _handleMessage:[frameData copy]]; 879 | break; 880 | case SROpCodeConnectionClose: 881 | [self handleCloseWithData:frameData]; 882 | break; 883 | case SROpCodePing: 884 | [self handlePing:frameData]; 885 | break; 886 | case SROpCodePong: 887 | [self handlePong:frameData]; 888 | break; 889 | default: 890 | [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; 891 | // TODO: Handle invalid opcode 892 | break; 893 | } 894 | } 895 | 896 | - (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; 897 | { 898 | assert(frame_header.opcode != 0); 899 | 900 | if (self.readyState != SR_OPEN) { 901 | return; 902 | } 903 | 904 | 905 | BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose); 906 | 907 | if (isControlFrame && !frame_header.fin) { 908 | [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; 909 | return; 910 | } 911 | 912 | if (isControlFrame && frame_header.payload_length >= 126) { 913 | [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; 914 | return; 915 | } 916 | 917 | if (!isControlFrame) { 918 | _currentFrameOpcode = frame_header.opcode; 919 | _currentFrameCount += 1; 920 | } 921 | 922 | if (frame_header.payload_length == 0) { 923 | if (isControlFrame) { 924 | [self _handleFrameWithData:curData opCode:frame_header.opcode]; 925 | } else { 926 | if (frame_header.fin) { 927 | [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; 928 | } else { 929 | // TODO add assert that opcode is not a control; 930 | [self _readFrameContinue]; 931 | } 932 | } 933 | } else { 934 | assert(frame_header.payload_length <= SIZE_T_MAX); 935 | [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) { 936 | if (isControlFrame) { 937 | [self _handleFrameWithData:newData opCode:frame_header.opcode]; 938 | } else { 939 | if (frame_header.fin) { 940 | [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; 941 | } else { 942 | // TODO add assert that opcode is not a control; 943 | [self _readFrameContinue]; 944 | } 945 | 946 | } 947 | } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; 948 | } 949 | } 950 | 951 | /* From RFC: 952 | 953 | 0 1 2 3 954 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 955 | +-+-+-+-+-------+-+-------------+-------------------------------+ 956 | |F|R|R|R| opcode|M| Payload len | Extended payload length | 957 | |I|S|S|S| (4) |A| (7) | (16/64) | 958 | |N|V|V|V| |S| | (if payload len==126/127) | 959 | | |1|2|3| |K| | | 960 | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 961 | | Extended payload length continued, if payload len == 127 | 962 | + - - - - - - - - - - - - - - - +-------------------------------+ 963 | | |Masking-key, if MASK set to 1 | 964 | +-------------------------------+-------------------------------+ 965 | | Masking-key (continued) | Payload Data | 966 | +-------------------------------- - - - - - - - - - - - - - - - + 967 | : Payload Data continued ... : 968 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 969 | | Payload Data continued ... | 970 | +---------------------------------------------------------------+ 971 | */ 972 | 973 | static const uint8_t SRFinMask = 0x80; 974 | static const uint8_t SROpCodeMask = 0x0F; 975 | static const uint8_t SRRsvMask = 0x70; 976 | static const uint8_t SRMaskMask = 0x80; 977 | static const uint8_t SRPayloadLenMask = 0x7F; 978 | 979 | 980 | - (void)_readFrameContinue; 981 | { 982 | assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); 983 | 984 | [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) { 985 | __block frame_header header = {0}; 986 | 987 | const uint8_t *headerBuffer = data.bytes; 988 | assert(data.length >= 2); 989 | 990 | if (headerBuffer[0] & SRRsvMask) { 991 | [self _closeWithProtocolError:@"Server used RSV bits"]; 992 | return; 993 | } 994 | 995 | uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); 996 | 997 | BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); 998 | 999 | if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { 1000 | [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; 1001 | return; 1002 | } 1003 | 1004 | if (receivedOpcode == 0 && self->_currentFrameCount == 0) { 1005 | [self _closeWithProtocolError:@"cannot continue a message"]; 1006 | return; 1007 | } 1008 | 1009 | header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; 1010 | 1011 | header.fin = !!(SRFinMask & headerBuffer[0]); 1012 | 1013 | 1014 | header.masked = !!(SRMaskMask & headerBuffer[1]); 1015 | header.payload_length = SRPayloadLenMask & headerBuffer[1]; 1016 | 1017 | headerBuffer = NULL; 1018 | 1019 | if (header.masked) { 1020 | [self _closeWithProtocolError:@"Client must receive unmasked data"]; 1021 | } 1022 | 1023 | size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; 1024 | 1025 | if (header.payload_length == 126) { 1026 | extra_bytes_needed += sizeof(uint16_t); 1027 | } else if (header.payload_length == 127) { 1028 | extra_bytes_needed += sizeof(uint64_t); 1029 | } 1030 | 1031 | if (extra_bytes_needed == 0) { 1032 | [self _handleFrameHeader:header curData:self->_currentFrameData]; 1033 | } else { 1034 | [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { 1035 | size_t mapped_size = data.length; 1036 | #pragma unused (mapped_size) 1037 | const void *mapped_buffer = data.bytes; 1038 | size_t offset = 0; 1039 | 1040 | if (header.payload_length == 126) { 1041 | assert(mapped_size >= sizeof(uint16_t)); 1042 | uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); 1043 | header.payload_length = newLen; 1044 | offset += sizeof(uint16_t); 1045 | } else if (header.payload_length == 127) { 1046 | assert(mapped_size >= sizeof(uint64_t)); 1047 | header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); 1048 | offset += sizeof(uint64_t); 1049 | } else { 1050 | assert(header.payload_length < 126 && header.payload_length >= 0); 1051 | } 1052 | 1053 | if (header.masked) { 1054 | assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); 1055 | memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); 1056 | } 1057 | 1058 | [self _handleFrameHeader:header curData:self->_currentFrameData]; 1059 | } readToCurrentFrame:NO unmaskBytes:NO]; 1060 | } 1061 | } readToCurrentFrame:NO unmaskBytes:NO]; 1062 | } 1063 | 1064 | - (void)_readFrameNew; 1065 | { 1066 | dispatch_async(_workQueue, ^{ 1067 | [_currentFrameData setLength:0]; 1068 | 1069 | _currentFrameOpcode = 0; 1070 | _currentFrameCount = 0; 1071 | _readOpCount = 0; 1072 | _currentStringScanPosition = 0; 1073 | 1074 | [self _readFrameContinue]; 1075 | }); 1076 | } 1077 | 1078 | - (void)_pumpWriting; 1079 | { 1080 | [self assertOnWorkQueue]; 1081 | 1082 | NSUInteger dataLength = _outputBuffer.length; 1083 | if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { 1084 | NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; 1085 | if (bytesWritten == -1) { 1086 | [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; 1087 | return; 1088 | } 1089 | 1090 | _outputBufferOffset += bytesWritten; 1091 | 1092 | if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { 1093 | _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; 1094 | _outputBufferOffset = 0; 1095 | } 1096 | } 1097 | 1098 | if (_closeWhenFinishedWriting && 1099 | _outputBuffer.length - _outputBufferOffset == 0 && 1100 | (_inputStream.streamStatus != NSStreamStatusNotOpen && 1101 | _inputStream.streamStatus != NSStreamStatusClosed) && 1102 | !_sentClose) { 1103 | _sentClose = YES; 1104 | 1105 | [_outputStream close]; 1106 | [_inputStream close]; 1107 | 1108 | 1109 | for (NSArray *runLoop in [_scheduledRunloops copy]) { 1110 | [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; 1111 | } 1112 | 1113 | if (!_failed) { 1114 | [self _performDelegateBlock:^{ 1115 | if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { 1116 | [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; 1117 | } 1118 | }]; 1119 | } 1120 | 1121 | _selfRetain = nil; 1122 | } 1123 | } 1124 | 1125 | - (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; 1126 | { 1127 | [self assertOnWorkQueue]; 1128 | [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; 1129 | } 1130 | 1131 | - (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; 1132 | { 1133 | [self assertOnWorkQueue]; 1134 | assert(dataLength); 1135 | 1136 | [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; 1137 | [self _pumpScanner]; 1138 | } 1139 | 1140 | - (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; 1141 | { 1142 | [self assertOnWorkQueue]; 1143 | [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; 1144 | [self _pumpScanner]; 1145 | } 1146 | 1147 | 1148 | static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; 1149 | 1150 | - (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; 1151 | { 1152 | [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; 1153 | } 1154 | 1155 | - (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; 1156 | { 1157 | // TODO optimize so this can continue from where we last searched 1158 | stream_scanner consumer = ^size_t(NSData *data) { 1159 | __block size_t found_size = 0; 1160 | __block size_t match_count = 0; 1161 | 1162 | size_t size = data.length; 1163 | const unsigned char *buffer = data.bytes; 1164 | for (size_t i = 0; i < size; i++ ) { 1165 | if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { 1166 | match_count += 1; 1167 | if (match_count == length) { 1168 | found_size = i + 1; 1169 | break; 1170 | } 1171 | } else { 1172 | match_count = 0; 1173 | } 1174 | } 1175 | return found_size; 1176 | }; 1177 | [self _addConsumerWithScanner:consumer callback:dataHandler]; 1178 | } 1179 | 1180 | 1181 | // Returns true if did work 1182 | - (BOOL)_innerPumpScanner { 1183 | 1184 | BOOL didWork = NO; 1185 | 1186 | if (self.readyState >= SR_CLOSING) { 1187 | return didWork; 1188 | } 1189 | 1190 | if (!_consumers.count) { 1191 | return didWork; 1192 | } 1193 | 1194 | size_t curSize = _readBuffer.length - _readBufferOffset; 1195 | if (!curSize) { 1196 | return didWork; 1197 | } 1198 | 1199 | SRIOConsumer *consumer = [_consumers objectAtIndex:0]; 1200 | 1201 | size_t bytesNeeded = consumer.bytesNeeded; 1202 | 1203 | size_t foundSize = 0; 1204 | if (consumer.consumer) { 1205 | NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; 1206 | foundSize = consumer.consumer(tempView); 1207 | } else { 1208 | assert(consumer.bytesNeeded); 1209 | if (curSize >= bytesNeeded) { 1210 | foundSize = bytesNeeded; 1211 | } else if (consumer.readToCurrentFrame) { 1212 | foundSize = curSize; 1213 | } 1214 | } 1215 | 1216 | NSData *slice = nil; 1217 | if (consumer.readToCurrentFrame || foundSize) { 1218 | NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); 1219 | slice = [_readBuffer subdataWithRange:sliceRange]; 1220 | 1221 | _readBufferOffset += foundSize; 1222 | 1223 | if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { 1224 | _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; 1225 | } 1226 | 1227 | if (consumer.unmaskBytes) { 1228 | NSMutableData *mutableSlice = [slice mutableCopy]; 1229 | 1230 | NSUInteger len = mutableSlice.length; 1231 | uint8_t *bytes = mutableSlice.mutableBytes; 1232 | 1233 | for (NSUInteger i = 0; i < len; i++) { 1234 | bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; 1235 | _currentReadMaskOffset += 1; 1236 | } 1237 | 1238 | slice = mutableSlice; 1239 | } 1240 | 1241 | if (consumer.readToCurrentFrame) { 1242 | [_currentFrameData appendData:slice]; 1243 | 1244 | _readOpCount += 1; 1245 | 1246 | if (_currentFrameOpcode == SROpCodeTextFrame) { 1247 | // Validate UTF8 stuff. 1248 | size_t currentDataSize = _currentFrameData.length; 1249 | if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) { 1250 | // TODO: Optimize the crap out of this. Don't really have to copy all the data each time 1251 | 1252 | size_t scanSize = currentDataSize - _currentStringScanPosition; 1253 | 1254 | NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; 1255 | int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); 1256 | 1257 | if (valid_utf8_size == -1) { 1258 | [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; 1259 | dispatch_async(_workQueue, ^{ 1260 | [self _disconnect]; 1261 | }); 1262 | return didWork; 1263 | } else { 1264 | _currentStringScanPosition += valid_utf8_size; 1265 | } 1266 | } 1267 | 1268 | } 1269 | 1270 | consumer.bytesNeeded -= foundSize; 1271 | 1272 | if (consumer.bytesNeeded == 0) { 1273 | [_consumers removeObjectAtIndex:0]; 1274 | consumer.handler(self, nil); 1275 | [_consumerPool returnConsumer:consumer]; 1276 | didWork = YES; 1277 | } 1278 | } else if (foundSize) { 1279 | [_consumers removeObjectAtIndex:0]; 1280 | consumer.handler(self, slice); 1281 | [_consumerPool returnConsumer:consumer]; 1282 | didWork = YES; 1283 | } 1284 | } 1285 | return didWork; 1286 | } 1287 | 1288 | -(void)_pumpScanner; 1289 | { 1290 | [self assertOnWorkQueue]; 1291 | 1292 | if (!_isPumping) { 1293 | _isPumping = YES; 1294 | } else { 1295 | return; 1296 | } 1297 | 1298 | while ([self _innerPumpScanner]) { 1299 | 1300 | } 1301 | 1302 | _isPumping = NO; 1303 | } 1304 | 1305 | //#define NOMASK 1306 | 1307 | static const size_t SRFrameHeaderOverhead = 32; 1308 | 1309 | - (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; 1310 | { 1311 | [self assertOnWorkQueue]; 1312 | 1313 | if (nil == data) { 1314 | return; 1315 | } 1316 | 1317 | NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); 1318 | 1319 | size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; 1320 | 1321 | NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; 1322 | if (!frame) { 1323 | [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; 1324 | return; 1325 | } 1326 | uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; 1327 | 1328 | // set fin 1329 | frame_buffer[0] = SRFinMask | opcode; 1330 | 1331 | BOOL useMask = YES; 1332 | #ifdef NOMASK 1333 | useMask = NO; 1334 | #endif 1335 | 1336 | if (useMask) { 1337 | // set the mask and header 1338 | frame_buffer[1] |= SRMaskMask; 1339 | } 1340 | 1341 | size_t frame_buffer_size = 2; 1342 | 1343 | const uint8_t *unmasked_payload = NULL; 1344 | if ([data isKindOfClass:[NSData class]]) { 1345 | unmasked_payload = (uint8_t *)[data bytes]; 1346 | } else if ([data isKindOfClass:[NSString class]]) { 1347 | unmasked_payload = (const uint8_t *)[data UTF8String]; 1348 | } else { 1349 | return; 1350 | } 1351 | 1352 | if (payloadLength < 126) { 1353 | frame_buffer[1] |= payloadLength; 1354 | } else if (payloadLength <= UINT16_MAX) { 1355 | frame_buffer[1] |= 126; 1356 | *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); 1357 | frame_buffer_size += sizeof(uint16_t); 1358 | } else { 1359 | frame_buffer[1] |= 127; 1360 | *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); 1361 | frame_buffer_size += sizeof(uint64_t); 1362 | } 1363 | 1364 | if (!useMask) { 1365 | for (size_t i = 0; i < payloadLength; i++) { 1366 | frame_buffer[frame_buffer_size] = unmasked_payload[i]; 1367 | frame_buffer_size += 1; 1368 | } 1369 | } else { 1370 | uint8_t *mask_key = frame_buffer + frame_buffer_size; 1371 | SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); 1372 | frame_buffer_size += sizeof(uint32_t); 1373 | 1374 | // TODO: could probably optimize this with SIMD 1375 | for (size_t i = 0; i < payloadLength; i++) { 1376 | frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; 1377 | frame_buffer_size += 1; 1378 | } 1379 | } 1380 | 1381 | assert(frame_buffer_size <= [frame length]); 1382 | frame.length = frame_buffer_size; 1383 | 1384 | [self _writeData:frame]; 1385 | } 1386 | 1387 | - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; 1388 | { 1389 | if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { 1390 | 1391 | NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; 1392 | if (sslCerts) { 1393 | SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; 1394 | if (secTrust) { 1395 | NSInteger numCerts = SecTrustGetCertificateCount(secTrust); 1396 | for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { 1397 | SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); 1398 | NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); 1399 | 1400 | for (id ref in sslCerts) { 1401 | SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; 1402 | NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); 1403 | 1404 | if ([trustedCertData isEqualToData:certData]) { 1405 | _pinnedCertFound = YES; 1406 | break; 1407 | } 1408 | } 1409 | } 1410 | } 1411 | 1412 | if (!_pinnedCertFound) { 1413 | dispatch_async(_workQueue, ^{ 1414 | [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; 1415 | }); 1416 | return; 1417 | } 1418 | } 1419 | } 1420 | 1421 | dispatch_async(_workQueue, ^{ 1422 | switch (eventCode) { 1423 | case NSStreamEventOpenCompleted: { 1424 | SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); 1425 | if (self.readyState >= SR_CLOSING) { 1426 | return; 1427 | } 1428 | assert(_readBuffer); 1429 | 1430 | if (self.readyState == SR_CONNECTING && aStream == _inputStream) { 1431 | [self didConnect]; 1432 | } 1433 | [self _pumpWriting]; 1434 | [self _pumpScanner]; 1435 | break; 1436 | } 1437 | 1438 | case NSStreamEventErrorOccurred: { 1439 | SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); 1440 | /// TODO specify error better! 1441 | [self _failWithError:aStream.streamError]; 1442 | _readBufferOffset = 0; 1443 | [_readBuffer setLength:0]; 1444 | break; 1445 | 1446 | } 1447 | 1448 | case NSStreamEventEndEncountered: { 1449 | [self _pumpScanner]; 1450 | SRFastLog(@"NSStreamEventEndEncountered %@", aStream); 1451 | if (aStream.streamError) { 1452 | [self _failWithError:aStream.streamError]; 1453 | } else { 1454 | if (self.readyState != SR_CLOSED) { 1455 | self.readyState = SR_CLOSED; 1456 | _selfRetain = nil; 1457 | } 1458 | 1459 | if (!_sentClose && !_failed) { 1460 | _sentClose = YES; 1461 | // If we get closed in this state it's probably not clean because we should be sending this when we send messages 1462 | [self _performDelegateBlock:^{ 1463 | if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { 1464 | [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; 1465 | } 1466 | }]; 1467 | } 1468 | } 1469 | 1470 | break; 1471 | } 1472 | 1473 | case NSStreamEventHasBytesAvailable: { 1474 | SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); 1475 | const int bufferSize = 2048; 1476 | uint8_t buffer[bufferSize]; 1477 | 1478 | while (_inputStream.hasBytesAvailable) { 1479 | NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; 1480 | 1481 | if (bytes_read > 0) { 1482 | [_readBuffer appendBytes:buffer length:bytes_read]; 1483 | } else if (bytes_read < 0) { 1484 | [self _failWithError:_inputStream.streamError]; 1485 | } 1486 | 1487 | if (bytes_read != bufferSize) { 1488 | break; 1489 | } 1490 | }; 1491 | [self _pumpScanner]; 1492 | break; 1493 | } 1494 | 1495 | case NSStreamEventHasSpaceAvailable: { 1496 | SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); 1497 | [self _pumpWriting]; 1498 | break; 1499 | } 1500 | 1501 | default: 1502 | SRFastLog(@"(default) %@", aStream); 1503 | break; 1504 | } 1505 | }); 1506 | } 1507 | 1508 | @end 1509 | 1510 | 1511 | @implementation SRIOConsumer 1512 | 1513 | @synthesize bytesNeeded = _bytesNeeded; 1514 | @synthesize consumer = _scanner; 1515 | @synthesize handler = _handler; 1516 | @synthesize readToCurrentFrame = _readToCurrentFrame; 1517 | @synthesize unmaskBytes = _unmaskBytes; 1518 | 1519 | - (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; 1520 | { 1521 | _scanner = [scanner copy]; 1522 | _handler = [handler copy]; 1523 | _bytesNeeded = bytesNeeded; 1524 | _readToCurrentFrame = readToCurrentFrame; 1525 | _unmaskBytes = unmaskBytes; 1526 | assert(_scanner || _bytesNeeded); 1527 | } 1528 | 1529 | 1530 | @end 1531 | 1532 | 1533 | @implementation SRIOConsumerPool { 1534 | NSUInteger _poolSize; 1535 | NSMutableArray *_bufferedConsumers; 1536 | } 1537 | 1538 | - (id)initWithBufferCapacity:(NSUInteger)poolSize; 1539 | { 1540 | self = [super init]; 1541 | if (self) { 1542 | _poolSize = poolSize; 1543 | _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; 1544 | } 1545 | return self; 1546 | } 1547 | 1548 | - (id)init 1549 | { 1550 | return [self initWithBufferCapacity:8]; 1551 | } 1552 | 1553 | - (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; 1554 | { 1555 | SRIOConsumer *consumer = nil; 1556 | if (_bufferedConsumers.count) { 1557 | consumer = [_bufferedConsumers lastObject]; 1558 | [_bufferedConsumers removeLastObject]; 1559 | } else { 1560 | consumer = [[SRIOConsumer alloc] init]; 1561 | } 1562 | 1563 | [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; 1564 | 1565 | return consumer; 1566 | } 1567 | 1568 | - (void)returnConsumer:(SRIOConsumer *)consumer; 1569 | { 1570 | if (_bufferedConsumers.count < _poolSize) { 1571 | [_bufferedConsumers addObject:consumer]; 1572 | } 1573 | } 1574 | 1575 | @end 1576 | 1577 | 1578 | @implementation NSURLRequest (CertificateAdditions) 1579 | 1580 | - (NSArray *)SR_SSLPinnedCertificates; 1581 | { 1582 | return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; 1583 | } 1584 | 1585 | @end 1586 | 1587 | @implementation NSMutableURLRequest (CertificateAdditions) 1588 | 1589 | - (NSArray *)SR_SSLPinnedCertificates; 1590 | { 1591 | return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; 1592 | } 1593 | 1594 | - (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; 1595 | { 1596 | [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; 1597 | } 1598 | 1599 | @end 1600 | 1601 | @implementation NSURL (SRWebSocket) 1602 | 1603 | - (NSString *)SR_origin; 1604 | { 1605 | NSString *scheme = [self.scheme lowercaseString]; 1606 | 1607 | if ([scheme isEqualToString:@"wss"]) { 1608 | scheme = @"https"; 1609 | } else if ([scheme isEqualToString:@"ws"]) { 1610 | scheme = @"http"; 1611 | } 1612 | 1613 | if (self.port) { 1614 | return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; 1615 | } else { 1616 | return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; 1617 | } 1618 | } 1619 | 1620 | @end 1621 | 1622 | //#define SR_ENABLE_LOG 1623 | 1624 | static inline void SRFastLog(NSString *format, ...) { 1625 | #ifdef SR_ENABLE_LOG 1626 | __block va_list arg_list; 1627 | va_start (arg_list, format); 1628 | 1629 | NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; 1630 | 1631 | va_end(arg_list); 1632 | 1633 | NSLog(@"[SR] %@", formattedString); 1634 | #endif 1635 | } 1636 | 1637 | 1638 | #ifdef HAS_ICU 1639 | 1640 | static inline int32_t validate_dispatch_data_partial_string(NSData *data) { 1641 | if ([data length] > INT32_MAX) { 1642 | // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere. 1643 | return -1; 1644 | } 1645 | 1646 | int32_t size = (int32_t)[data length]; 1647 | 1648 | const void * contents = [data bytes]; 1649 | const uint8_t *str = (const uint8_t *)contents; 1650 | 1651 | UChar32 codepoint = 1; 1652 | int32_t offset = 0; 1653 | int32_t lastOffset = 0; 1654 | while(offset < size && codepoint > 0) { 1655 | lastOffset = offset; 1656 | U8_NEXT(str, offset, size, codepoint); 1657 | } 1658 | 1659 | if (codepoint == -1) { 1660 | // Check to see if the last byte is valid or whether it was just continuing 1661 | if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) { 1662 | 1663 | size = -1; 1664 | } else { 1665 | uint8_t leadByte = str[lastOffset]; 1666 | U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte)); 1667 | 1668 | for (int i = lastOffset + 1; i < offset; i++) { 1669 | if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) { 1670 | size = -1; 1671 | } 1672 | } 1673 | 1674 | if (size != -1) { 1675 | size = lastOffset; 1676 | } 1677 | } 1678 | } 1679 | 1680 | if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) { 1681 | size = -1; 1682 | } 1683 | 1684 | return size; 1685 | } 1686 | 1687 | #else 1688 | 1689 | // This is a hack, and probably not optimal 1690 | static inline int32_t validate_dispatch_data_partial_string(NSData *data) { 1691 | static const int maxCodepointSize = 3; 1692 | 1693 | for (int i = 0; i < maxCodepointSize; i++) { 1694 | NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; 1695 | if (str) { 1696 | return data.length - i; 1697 | } 1698 | } 1699 | 1700 | return -1; 1701 | } 1702 | 1703 | #endif 1704 | 1705 | static _SRRunLoopThread *networkThread = nil; 1706 | static NSRunLoop *networkRunLoop = nil; 1707 | 1708 | @implementation NSRunLoop (SRWebSocket) 1709 | 1710 | + (NSRunLoop *)SR_networkRunLoop { 1711 | static dispatch_once_t onceToken; 1712 | dispatch_once(&onceToken, ^{ 1713 | networkThread = [[_SRRunLoopThread alloc] init]; 1714 | networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; 1715 | [networkThread start]; 1716 | networkRunLoop = networkThread.runLoop; 1717 | }); 1718 | 1719 | return networkRunLoop; 1720 | } 1721 | 1722 | @end 1723 | 1724 | 1725 | @implementation _SRRunLoopThread { 1726 | dispatch_group_t _waitGroup; 1727 | } 1728 | 1729 | @synthesize runLoop = _runLoop; 1730 | 1731 | - (void)dealloc 1732 | { 1733 | sr_dispatch_release(_waitGroup); 1734 | } 1735 | 1736 | - (id)init 1737 | { 1738 | self = [super init]; 1739 | if (self) { 1740 | _waitGroup = dispatch_group_create(); 1741 | dispatch_group_enter(_waitGroup); 1742 | } 1743 | return self; 1744 | } 1745 | 1746 | - (void)main; 1747 | { 1748 | @autoreleasepool { 1749 | _runLoop = [NSRunLoop currentRunLoop]; 1750 | dispatch_group_leave(_waitGroup); 1751 | 1752 | NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; 1753 | [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; 1754 | 1755 | while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { 1756 | 1757 | } 1758 | assert(NO); 1759 | } 1760 | } 1761 | 1762 | - (NSRunLoop *)runLoop; 1763 | { 1764 | dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); 1765 | return _runLoop; 1766 | } 1767 | 1768 | @end 1769 | -------------------------------------------------------------------------------- /src/ios/ortc/Balancer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Balancer.h 3 | // OrtcClient 4 | // 5 | // Created by Marcin Kulwikowski on 15/07/14. 6 | // 7 | // 8 | 9 | #import 10 | 11 | @interface Balancer : NSObject 12 | 13 | - initWithCluster:(NSString*) aCluster serverUrl:(NSString*)url isCluster:(BOOL)isCluster appKey:(NSString*) anAppKey callback:(void (^)(NSString *aBalancerResponse))aCallback; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /src/ios/ortc/Balancer.m: -------------------------------------------------------------------------------- 1 | // 2 | // Balancer.m 3 | // OrtcClient 4 | // 5 | // Created by Marcin Kulwikowski on 15/07/14. 6 | // 7 | // 8 | 9 | #import "Balancer.h" 10 | 11 | NSString* const BALANCER_RESPONSE_PATTERN = @"^var SOCKET_SERVER = \\\"(.*?)\\\";$"; 12 | 13 | @implementation Balancer { 14 | NSMutableData *receivedData; 15 | void (^theCallabck)(NSString*); 16 | } 17 | 18 | 19 | - (id) initWithCluster:(NSString*) aCluster serverUrl:(NSString*)url isCluster:(BOOL)isCluster appKey:(NSString*) anAppKey callback:(void (^)(NSString *aBalancerResponse))aCallback{ 20 | if ((self = [super init])) { 21 | theCallabck = aCallback; 22 | NSString* parsedUrl = aCluster; 23 | 24 | if(!isCluster){ 25 | aCallback(url); 26 | } else { 27 | if(anAppKey != NULL){ 28 | parsedUrl = [parsedUrl stringByAppendingString:@"?appkey="]; 29 | parsedUrl = [parsedUrl stringByAppendingString:anAppKey]; 30 | } 31 | NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:parsedUrl]]; 32 | (void)[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; 33 | } 34 | } 35 | return nil; 36 | } 37 | 38 | 39 | 40 | #pragma mark NSURLConnection delegate methods 41 | - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse*)cachedResponse { 42 | // Return nil to indicate not necessary to store a cached response for this connection 43 | return nil; 44 | } 45 | 46 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 47 | receivedData = [[NSMutableData alloc] init]; 48 | } 49 | 50 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 51 | [receivedData appendData:data]; 52 | } 53 | 54 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)aError { 55 | theCallabck(nil); 56 | } 57 | 58 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 59 | if(receivedData !=nil){ 60 | NSString* myString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding]; 61 | 62 | NSRegularExpression* resRegex = [NSRegularExpression regularExpressionWithPattern:BALANCER_RESPONSE_PATTERN options:0 error:NULL]; 63 | NSTextCheckingResult* resMatch = [resRegex firstMatchInString:myString options:0 range:NSMakeRange(0, [myString length])]; 64 | 65 | if (resMatch) 66 | { 67 | NSRange strRange = [resMatch rangeAtIndex:1]; 68 | 69 | if (strRange.location != NSNotFound) { 70 | theCallabck([myString substringWithRange:strRange]); 71 | return; 72 | } 73 | } 74 | } 75 | theCallabck(nil); 76 | 77 | } 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /src/ios/ortc/OrtcClient.h: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // OrtcClient.h 4 | // OrtcClient 5 | // 6 | // Created by Rafael Cabral on 2/2/12. 7 | // Copyright (c) 2012 IBT. All rights reserved. 8 | // 9 | #define NOTIFICATIONS_KEY @"Local_Storage_Notifications" 10 | 11 | #ifndef OrtcClient_OrtcClient_h 12 | #define OrtcClient_OrtcClient_h 13 | 14 | #import 15 | #import "SRWebSocket.h" 16 | 17 | #define heartbeatDefaultTime 15 // Heartbeat default interval time 18 | #define heartbeatDefaultFails 3 // Heartbeat default max fails 19 | 20 | #define heartbeatMaxTime 60 21 | #define heartbeatMinTime 10 22 | 23 | #define heartbeatMaxFails 6 24 | #define heartbeatMinFails 1 25 | 26 | @class OrtcClient; 27 | 28 | @protocol OrtcClientDelegate 29 | 30 | ///--------------------------------------------------------------------------------------- 31 | /// @name Instance Methods 32 | ///-------------------------------------------------------------------------------------- 33 | /** 34 | * Occurs when the client connects. 35 | * 36 | * @param ortc The ORTC object. 37 | */ 38 | - (void)onConnected:(OrtcClient*) ortc; 39 | /** 40 | * Occurs when the client disconnects. 41 | * 42 | * @param ortc The ORTC object. 43 | */ 44 | - (void)onDisconnected:(OrtcClient*) ortc; 45 | /** 46 | * Occurs when the client subscribes to a channel. 47 | * 48 | * @param ortc The ORTC object. 49 | * @param channel The channel name. 50 | */ 51 | - (void)onSubscribed:(OrtcClient*) ortc channel:(NSString*) channel; 52 | /** 53 | * Occurs when the client unsubscribes from a channel. 54 | * 55 | * @param ortc The ORTC object. 56 | * @param channel The channel name. 57 | */ 58 | - (void)onUnsubscribed:(OrtcClient*) ortc channel:(NSString*) channel; 59 | /** 60 | * Occurs when there is an exception. 61 | * 62 | * @param ortc The ORTC object. 63 | * @param error The occurred exception. 64 | */ 65 | - (void)onException:(OrtcClient*) ortc error:(NSError*) error; 66 | /** 67 | * Occurs when the client attempts to reconnect. 68 | * 69 | * @param ortc The ORTC object. 70 | */ 71 | - (void)onReconnecting:(OrtcClient*) ortc; 72 | /** 73 | * Occurs when the client reconnects. 74 | * 75 | * @param ortc The ORTC object. 76 | */ 77 | - (void)onReconnected:(OrtcClient*) ortc; 78 | /** 79 | * Occurs when the client enables presence. 80 | * 81 | * @param error Description of error if occurs. 82 | * @param result Result of enablePresence 83 | */ 84 | //- (void)onEnablePresence:(NSError*) error result:(NSString*) result; 85 | 86 | 87 | @end 88 | 89 | /** 90 | 91 | OrtcClient Usage - Code Example 92 | 93 | - ViewController.h 94 |

 95 |  #import "OrtcClient.h"
 96 |  @interface ViewController : UIViewController 
 97 |  {
 98 |  @private
 99 |  
100 |     OrtcClient* ortcClient;
101 |     void (^onMessage)(OrtcClient* ortc, NSString* channel, NSString* message);
102 |     // ...
103 |  }
104 |  // ...
105 |  @end
106 |  
107 |  
108 | 109 | 110 | 111 | - ViewController.m 112 | 113 |

114 |  #import "ViewController.h"
115 |  @implementation ViewController
116 |  
117 |  - (void)viewDidLoad
118 |  {
119 |     [super viewDidLoad];
120 |     
121 |     // Instantiate OrtcClient
122 |     ortcClient = [OrtcClient ortcClientWithConfig:self];
123 |     
124 |     // Post permissions
125 |     @try {
126 |         NSMutableDictionary* myPermissions = [[NSMutableDictionary alloc] init];
127 |         
128 |         [myPermissions setObject:@"w" forKey:@"channel1"];
129 |         [myPermissions setObject:@"w" forKey:@"channel2"];
130 |         [myPermissions setObject:@"r" forKey:@"channelread"];
131 |         
132 |         BOOL postResult = [ortcClient saveAuthentication:@"http://ortc_server"
133 |                             isCLuster:YES authenticationToken:@"myAuthenticationToken"
134 |                             authenticationTokenIsPrivate:NO applicationKey:@"myApplicationKey"
135 |                             timeToLive:1800 privateKey:@"myPrivateKey" permissions:myPermissions];
136 |         
137 |         if (postResult) {
138 |             // Permissions correctly posted
139 |         }
140 |         else {
141 |             // Unable to post permissions
142 |         }
143 |     }
144 |     @catch (NSException* exception) {
145 |         // Exception posting permissions
146 |     }
147 |     
148 |     // Set connection properties
149 |     [ortcClient setConnectionMetadata:@"clientConnMeta"];
150 |     [ortcClient setClusterUrl:@"http://ortc_server"];
151 |     
152 |     // Connect
153 |     [ortcClient connect:@"myApplicationKey" authenticationToken:@"myAuthenticationToken"];
154 |  }
155 |  
156 |  - (void) onConnected:(OrtcClient*) ortc
157 |  {
158 |     // Connected
159 |     onMessage = ^(OrtcClient* ortc, NSString* channel, NSString* message) {
160 |     // Received message 'message' at channel 'channel'
161 |         [ortcClient unsubscribe:channel];
162 |     };
163 |     
164 |     [ortcClient subscribe:@"channel1" subscribeOnReconnected:YES onMessage:onMessage];
165 |     [ortcClient subscribe:@"channel2" subscribeOnReconnected:NO onMessage:onMessage];
166 |     [ortcClient subscribeWithNotifications:@"channel3" subscribeOnReconnected:YES onMessage:onMessage];
167 | }
168 | 
169 |  - (void) onDisconnected:(OrtcClient*) ortc
170 |  {
171 |     // Disconnected
172 |  }
173 | 
174 |  - (void) onReconnecting:(OrtcClient*) ortc
175 |  {
176 |     // Trying to reconnect
177 |  }
178 |  
179 |  - (void) onReconnected:(OrtcClient*) ortc
180 |  {
181 |     // Reconnected
182 |  }
183 |  
184 |  - (void) onSubscribed:(OrtcClient*) ortc channel:(NSString*) channel
185 |  {
186 |     // Subscribed to the channel 'channel'
187 |     [ortcClient send:channel message:@"Message to the channel"];
188 |  }
189 | 
190 |  - (void) onUnsubscribed:(OrtcClient*) ortc channel:(NSString*) channel
191 |  {
192 |     // Unsubscribed from the channel 'channel'
193 |     [ortcClient disconnect];
194 |  }
195 | 
196 |  - (void) onException:(OrtcClient*) ortc error:(NSError*) error
197 |  {
198 |     // Exception occurred
199 |  }
200 | 
201 |  @end
202 |  
203 | */ 204 | 205 | 206 | 207 | @interface OrtcClient : NSObject 208 | 209 | ///--------------------------------------------------------------------------------------- 210 | /// @name Properties 211 | ///--------------------------------------------------------------------------------------- 212 | @property (nonatomic, retain) NSString* id; 213 | @property (nonatomic, retain) NSString* url; 214 | @property (nonatomic, retain) NSString* clusterUrl; 215 | @property (nonatomic, retain) NSString* connectionMetadata; 216 | @property (nonatomic, retain) NSString* announcementSubChannel; 217 | @property (nonatomic, retain) NSString* sessionId; 218 | @property (assign) int connectionTimeout; 219 | @property (assign) BOOL isConnected; 220 | 221 | 222 | ///--------------------------------------------------------------------------------------- 223 | /// @name Class Methods 224 | ///--------------------------------------------------------------------------------------- 225 | /** 226 | * Initializes a new instance of the ORTC class. 227 | * 228 | * @param delegate The object holding the ORTC callbacks, usually 'self'. 229 | * 230 | * @return New instance of the ORTC class. 231 | */ 232 | 233 | + (id)ortcClientWithConfig:(id) delegate; 234 | 235 | 236 | ///--------------------------------------------------------------------------------------- 237 | /// @name Instance Methods 238 | ///--------------------------------------------------------------------------------------- 239 | /** 240 | * Connects with the application key and authentication token. 241 | * 242 | * @param applicationKey The application key. 243 | * @param authenticationToken The authentication token. 244 | */ 245 | - (void)connect:(NSString*) applicationKey authenticationToken:(NSString*) authenticationToken; 246 | /** 247 | * Sends a message to a channel. 248 | * 249 | * @param channel The channel name. 250 | * @param message The message to send. 251 | */ 252 | - (void)send:(NSString*) channel message:(NSString*) message; 253 | /** 254 | * Subscribes to a channel to receive messages sent to it. 255 | * 256 | * @param channel The channel name. 257 | * @param subscribeOnReconnected Indicates whether the client should subscribe to the channel when reconnected (if it was previously subscribed when connected). 258 | * @param onMessage The callback called when a message arrives at the channel. 259 | */ 260 | - (void)subscribe:(NSString*) channel subscribeOnReconnected:(BOOL) aSubscribeOnReconnected onMessage:(void (^)(OrtcClient* ortc, NSString* channel, NSString* message)) onMessage; 261 | 262 | /** 263 | * Subscribes to a channel, with Push Notifications Service, to receive messages sent to it. 264 | * 265 | * @param channel The channel name. Only channels with alphanumeric name and the following characters: "_" "-" ":" are allowed. 266 | * @param subscribeOnReconnected Indicates whether the client should subscribe to the channel when reconnected (if it was previously subscribed when connected). 267 | * @param onMessage The callback called when a message or a Push Notification arrives at the channel. 268 | */ 269 | - (void)subscribeWithNotifications:(NSString*) channel subscribeOnReconnected:(BOOL) aSubscribeOnReconnected onMessage:(void (^)(OrtcClient* ortc, NSString* channel, NSString* message)) onMessage; 270 | 271 | /** 272 | * Unsubscribes from a channel to stop receiving messages sent to it. 273 | * 274 | * @param channel The channel name. 275 | */ 276 | - (void)unsubscribe:(NSString*) channel; 277 | /** 278 | * Disconnects. 279 | */ 280 | - (void)disconnect; 281 | /** 282 | * Indicates whether is subscribed to a channel or not. 283 | * 284 | * @param channel The channel name. 285 | * 286 | * @return TRUE if subscribed to the channel or FALSE if not. 287 | */ 288 | - (NSNumber*)isSubscribed:(NSString*) channel; 289 | 290 | /** Saves the channels and its permissions for the authentication token in the ORTC server. 291 | @warning This function will send your private key over the internet. Make sure to use secure connection. 292 | @param url ORTC server URL. 293 | @param isCluster Indicates whether the ORTC server is in a cluster. 294 | @param authenticationToken The authentication token generated by an application server (for instance: a unique session ID). 295 | @param authenticationTokenIsPrivate Indicates whether the authentication token is private (1) or not (0). 296 | @param applicationKey The application key provided together with the ORTC service purchasing. 297 | @param timeToLive The authentication token time to live (TTL), in other words, the allowed activity time (in seconds). 298 | @param privateKey The private key provided together with the ORTC service purchasing. 299 | @param permissions The channels and their permissions (w: write, r: read, p: presence, case sensitive). 300 | @return TRUE if the authentication was successful or FALSE if it was not. 301 | */ 302 | - (BOOL)saveAuthentication:(NSString*) url isCLuster:(BOOL) isCluster authenticationToken:(NSString*) authenticationToken authenticationTokenIsPrivate:(BOOL) authenticationTokenIsPrivate applicationKey:(NSString*) applicationKey timeToLive:(int) timeToLive privateKey:(NSString*) privateKey permissions:(NSMutableDictionary*) permissions; 303 | 304 | /** Enables presence for the specified channel with first 100 unique metadata if true. 305 | 306 | @warning This function will send your private key over the internet. Make sure to use secure connection. 307 | @param url Server containing the presence service. 308 | @param isCluster Specifies if url is cluster. 309 | @param applicationKey Application key with access to presence service. 310 | @param privateKey The private key provided when the ORTC service is purchased. 311 | @param channel Channel with presence data active. 312 | @param metadata Defines if to collect first 100 unique metadata. 313 | @param callback Callback with error (NSError) and result (NSString) parameters 314 | */ 315 | - (void)enablePresence:(NSString*) aUrl isCLuster:(BOOL) aIsCluster applicationKey:(NSString*) aApplicationKey privateKey:(NSString*) aPrivateKey channel:(NSString*) channel metadata:(BOOL) aMetadata callback:(void (^)(NSError* error, NSString* result)) aCallback; 316 | 317 | /** Disables presence for the specified channel. 318 | 319 | @warning This function will send your private key over the internet. Make sure to use secure connection. 320 | @param url Server containing the presence service. 321 | @param isCluster Specifies if url is cluster. 322 | @param applicationKey Application key with access to presence service. 323 | @param privateKey The private key provided when the ORTC service is purchased. 324 | @param channel Channel with presence data active. 325 | @param callback Callback with error (NSError) and result (NSString) parameters 326 | */ 327 | - (void)disablePresence:(NSString*) aUrl isCLuster:(BOOL) aIsCluster applicationKey:(NSString*) aApplicationKey privateKey:(NSString*) aPrivateKey channel:(NSString*) channel callback:(void (^)(NSError* error, NSString* result)) aCallback; 328 | 329 | /** 330 | * Gets a NSDictionary indicating the subscriptions in the specified channel and if active the first 100 unique metadata. 331 | * 332 | * @param url Server containing the presence service. 333 | * @param isCluster Specifies if url is cluster. 334 | * @param applicationKey Application key with access to presence service. 335 | * @param authenticationToken Authentication token with access to presence service. 336 | * @param channel Channel with presence data active. 337 | * @param callback Callback with error (NSError) and result (NSDictionary) parameters 338 | */ 339 | - (void)presence:(NSString*) aUrl isCLuster:(BOOL) aIsCluster applicationKey:(NSString*) aApplicationKey authenticationToken:(NSString*) aAuthenticationToken channel:(NSString*) channel callback:(void (^)(NSError* error, NSDictionary* result)) aCallback; 340 | 341 | /** 342 | * Get heartbeat interval. 343 | */ 344 | - (int) getHeartbeatTime; 345 | /** 346 | * Set heartbeat interval. 347 | */ 348 | - (void) setHeartbeatTime:(int) newHeartbeatTime; 349 | /** 350 | * Get how many times can the client fail the heartbeat. 351 | */ 352 | - (int) getHeartbeatFails; 353 | /** 354 | * Set heartbeat fails. Defines how many times can the client fail the heartbeat. 355 | */ 356 | - (void) setHeartbeatFails:(int) newHeartbeatFails; 357 | /** 358 | * Indicates whether heartbeat is active or not. 359 | */ 360 | - (BOOL) isHeartbeatActive; 361 | /** 362 | * Enables the client heartbeat 363 | */ 364 | - (void) enableHeartbeat; 365 | /** 366 | * Disables the client heartbeat 367 | */ 368 | - (void) disableHeartbeat; 369 | 370 | 371 | + (void) setDEVICE_TOKEN:(NSString *) deviceToken; 372 | @end 373 | 374 | 375 | #endif 376 | 377 | -------------------------------------------------------------------------------- /www/OrtcPlugin.js: -------------------------------------------------------------------------------- 1 | (function(cordova) { 2 | 3 | function OrtcPushPlugin() {} 4 | 5 | OrtcPushPlugin.prototype.checkForNotifications = function() { 6 | var promise = new Promise(function(resolve, reject) { 7 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "checkForNotifications", []); 8 | }); 9 | return promise; 10 | }; 11 | 12 | OrtcPushPlugin.prototype.removeNotifications = function() { 13 | var promise = new Promise(function(resolve, reject) { 14 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "removeNotifications", []); 15 | }); 16 | return promise; 17 | }; 18 | 19 | OrtcPushPlugin.prototype.connect = function(config) { 20 | var promise = new Promise(function(resolve, reject) { 21 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "connect", config ? [config] : []); 22 | }); 23 | return promise; 24 | }; 25 | 26 | OrtcPushPlugin.prototype.getIsConnected = function() { 27 | var promise = new Promise(function(resolve, reject) { 28 | cordova.exec(function(res){resolve(res);}, function(){reject();}, "OrtcPushPlugin", "getIsConnected", []); 29 | }); 30 | return promise; 31 | }; 32 | 33 | OrtcPushPlugin.prototype.enableHeadsUpNotifications = function() { 34 | var promise = new Promise(function(resolve, reject) { 35 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "enableHeadsUpNotifications", []); 36 | }); 37 | return promise; 38 | }; 39 | 40 | OrtcPushPlugin.prototype.disableHeadsUpNotifications = function() { 41 | var promise = new Promise(function(resolve, reject) { 42 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "disableHeadsUpNotifications", []); 43 | }); 44 | return promise; 45 | }; 46 | 47 | OrtcPushPlugin.prototype.disconnect = function() { 48 | var promise = new Promise(function(resolve, reject) { 49 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "disconnect", []); 50 | }); 51 | return promise; 52 | }; 53 | 54 | OrtcPushPlugin.prototype.subscribe = function(config) { 55 | var promise = new Promise(function(resolve, reject) { 56 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "subscribe", config ? [config] : []); 57 | }); 58 | return promise; 59 | }; 60 | 61 | 62 | OrtcPushPlugin.prototype.unsubscribe = function(config) { 63 | var promise = new Promise(function(resolve, reject) { 64 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "unsubscribe", config ? [config] : []); 65 | }); 66 | return promise; 67 | }; 68 | 69 | OrtcPushPlugin.prototype.setApplicationIconBadgeNumber = function(badge) { 70 | var promise = new Promise(function(resolve, reject) { 71 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "setApplicationIconBadgeNumber", [badge]); 72 | }); 73 | return promise; 74 | }; 75 | 76 | OrtcPushPlugin.prototype.send = function(config) { 77 | cordova.exec(null, null, "OrtcPushPlugin", "send", config ? [config] : []); 78 | }; 79 | 80 | // Call this to clear all notifications from the notification center 81 | OrtcPushPlugin.prototype.cancelAllLocalNotifications = function() { 82 | var promise = new Promise(function(resolve, reject) { 83 | cordova.exec(function(){resolve();}, function(){reject();}, "OrtcPushPlugin", "cancelAllLocalNotifications", []); 84 | }); 85 | return promise; 86 | }; 87 | 88 | OrtcPushPlugin.prototype.log = function(log) { 89 | cordova.exec(null, null, "OrtcPushPlugin", "log", log ? [log] : []); 90 | }; 91 | 92 | OrtcPushPlugin.prototype.receiveRemoteNotification = function(channel, payload, tapped) { 93 | var ev = document.createEvent('HTMLEvents'); 94 | ev.channel = channel; 95 | ev.payload = payload; 96 | ev.tapped = tapped; 97 | ev.initEvent('push-notification', true, true, arguments); 98 | document.dispatchEvent(ev); 99 | }; 100 | 101 | OrtcPushPlugin.prototype.onException = function(error){ 102 | var ev = document.createEvent('HTMLEvents'); 103 | ev.description = error; 104 | ev.initEvent('onException', true, true, arguments); 105 | document.dispatchEvent(ev); 106 | }; 107 | 108 | cordova.addConstructor(function() { 109 | if(!window.plugins) window.plugins = {}; 110 | window.plugins.OrtcPushPlugin = new OrtcPushPlugin(); 111 | }); 112 | 113 | })(window.cordova || window.Cordova || window.PhoneGap); 114 | 115 | 116 | // call when device is ready 117 | document.addEventListener("deviceready", function () { 118 | if(window.plugins && window.plugins.OrtcPushPlugin){ 119 | var OrtcPushPlugin = window.plugins.OrtcPushPlugin; 120 | OrtcPushPlugin.checkForNotifications(); 121 | 122 | } 123 | }); 124 | 125 | 126 | // call when app resumes 127 | document.addEventListener("resume", function () { 128 | if(window.plugins && window.plugins.OrtcPushPlugin){ 129 | var OrtcPushPlugin = window.plugins.OrtcPushPlugin; 130 | OrtcPushPlugin.checkForNotifications(); 131 | } 132 | }); 133 | 134 | 135 | --------------------------------------------------------------------------------