--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>SyncerClient</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.upsuper.playlistsyncer.client"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="16"
+ android:targetSdkVersion="18" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/playlist_syncer_icon"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name="org.upsuper.playlistsyncer.client.SelectActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="org.upsuper.playlistsyncer.client.SyncActivity"
+ android:label="@string/app_name" >
+ </activity>
+ </application>
+
+</manifest>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+ <issue id="ContentDescription" severity="ignore" />
+ <issue id="DefaultLocale" severity="ignore" />
+</lint>
\ No newline at end of file
--- /dev/null
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
--- /dev/null
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-18
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:id="@android:id/background" android:drawable="@color/progress_background" />
+ <item android:id="@android:id/progress">
+ <clip android:drawable="@color/progress_foreground"/>
+ </item>
+</layer-list>
--- /dev/null
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".SelectActivity" >
+
+ <ListView
+ android:id="@+id/list_service"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true" >
+ </ListView>
+
+</RelativeLayout>
--- /dev/null
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".SyncActivity" >
+
+ <ListView
+ android:id="@+id/list_file"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true" >
+ </ListView>
+
+</RelativeLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context=".SyncActivity" >
+
+ <ImageView
+ android:id="@+id/image_status"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="5dp"
+ android:src="@drawable/ic_pending" />
+
+ <TextView
+ android:id="@+id/text_filename"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/image_status"
+ android:layout_toLeftOf="@+id/text_progress"
+ android:layout_toRightOf="@+id/image_status"
+ android:layout_marginRight="5dp"
+ android:ellipsize="end"
+ android:textSize="16sp"
+ android:singleLine="true"
+ android:text="" />
+
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="@android:style/Widget.ProgressBar.Horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="2dp"
+ android:layout_alignBottom="@+id/image_status"
+ android:layout_alignParentRight="true"
+ android:layout_toRightOf="@+id/image_status"
+ android:progressDrawable="@drawable/progress"
+ android:max="100"
+ android:progress="50" />
+
+ <TextView
+ android:id="@+id/text_filesize"
+ style="@style/Text.Appearance.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/image_status"
+ android:layout_alignRight="@+id/progress_bar"
+ android:layout_marginBottom="7dp"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/text_progress"
+ style="@style/Text.Appearance.Small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/text_filesize"
+ android:layout_toLeftOf="@+id/text_filesize"
+ android:layout_marginRight="7dp"
+ android:text="" />
+
+</RelativeLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin" />
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <color name="progress_background">#cccccc</color>
+ <color name="progress_foreground">#0099cc</color>
+
+</resources>
--- /dev/null
+<resources>
+
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Playlist Syncer</string>
+
+</resources>
--- /dev/null
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+ <style name="Text.Appearance.Small" parent="">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#666666</item>
+ </style>
+
+</resources>
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.io.File;
+
+import android.os.Environment;
+
+public interface Constants {
+
+ public static final String PACKAGE_NAME = Constants.class.getPackage().getName();
+ public static final File MUSIC_DIRECTORY =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.io.File;
+import java.util.List;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class FileListAdapter extends BaseAdapter {
+
+ private List<String> fileList;
+ private int[] fileSizeList;
+ private LayoutInflater inflater;
+
+ private int currentIndex = -1;
+ private int currentReceived = 0;
+
+ public FileListAdapter(Context context, List<String> list) {
+ fileList = list;
+ fileSizeList = new int[list.size()];
+ inflater = (LayoutInflater)
+ context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return fileList.size();
+ }
+
+ @Override
+ public String getItem(int position) {
+ String file = fileList.get(position);
+ int pos = file.lastIndexOf(File.separatorChar);
+ return file.substring(pos + 1);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ public void setCurrentPosition(int position, int size) {
+ currentIndex = position;
+ if (position < fileSizeList.length)
+ fileSizeList[position] = size;
+ currentReceived = 0;
+ notifyDataSetChanged();
+ }
+
+ public void updateProgress(int received) {
+ currentReceived = received;
+ notifyDataSetChanged();
+ }
+
+ public static String humanReadableSize(long bytes, boolean si) {
+ int unit = si ? 1000 : 1024;
+ if (bytes < unit) return bytes + " B";
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
+ String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
+ return String.format("%.2f %sB", bytes / Math.pow(unit, exp), pre);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+ if (v == null)
+ v = inflater.inflate(R.layout.view_file_item, parent, false);
+
+ ViewHolder h = (ViewHolder) v.getTag();
+ if (h == null) {
+ h = new ViewHolder();
+ h.imageStatus = (ImageView) v.findViewById(R.id.image_status);
+ h.textFilename = (TextView) v.findViewById(R.id.text_filename);
+ h.textFilesize = (TextView) v.findViewById(R.id.text_filesize);
+ h.textProgress = (TextView) v.findViewById(R.id.text_progress);
+ h.progressBar = (ProgressBar) v.findViewById(R.id.progress_bar);
+ v.setTag(h);
+ }
+
+ h.textFilename.setText(getItem(position));
+ h.textProgress.setText("");
+ h.progressBar.setVisibility(View.INVISIBLE);
+
+ int size = fileSizeList[position];
+ if (position <= currentIndex && size > 0) {
+ String readableSize = humanReadableSize(size, false);
+ h.textFilesize.setText(readableSize);
+ } else {
+ h.textFilesize.setText("");
+ }
+
+ if (position < currentIndex) {
+ h.imageStatus.setImageResource(R.drawable.ic_complete);
+ } else if (position > currentIndex || size == 0) {
+ h.imageStatus.setImageResource(R.drawable.ic_pending);
+ } else {
+ h.imageStatus.setImageResource(R.drawable.ic_downloading);
+
+ h.progressBar.setVisibility(View.VISIBLE);
+ h.progressBar.setMax(size);
+ h.progressBar.setProgress(currentReceived);
+
+ int percentage = currentReceived * 100 / size;
+ String progress = String.format("%d%%", percentage);
+ h.textProgress.setText(progress);
+ }
+
+ return v;
+ }
+
+ private class ViewHolder {
+ public ImageView imageStatus;
+ public TextView textFilename;
+ public TextView textFilesize;
+ public TextView textProgress;
+ public ProgressBar progressBar;
+ }
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.util.Log;
+
+public class MediaScannerClient implements MediaScannerConnectionClient {
+
+ private MediaScannerConnection scanner;
+ private ScannerListener listener;
+
+ private List<String> fileToScan;
+ private List<String> fileScanning;
+
+ public MediaScannerClient(Context context) {
+ fileToScan = new LinkedList<String>();
+ fileScanning = new LinkedList<String>();
+
+ scanner = new MediaScannerConnection(context, this);
+ scanner.connect();
+ }
+
+ private synchronized void scanFile(String path) {
+ scanner.scanFile(path, null);
+ fileScanning.add(path);
+ }
+
+ public synchronized void scanFile(File file) {
+ if (scanner.isConnected()) {
+ scanFile(file.getAbsolutePath());
+ } else {
+ fileToScan.add(file.getAbsolutePath());
+ }
+ }
+
+ @Override
+ public synchronized void onMediaScannerConnected() {
+ for (String file : fileToScan)
+ scanFile(file);
+ fileToScan.clear();
+ }
+
+ @Override
+ public synchronized void onScanCompleted(String path, Uri uri) {
+ if (!fileScanning.remove(path))
+ Log.w("MediaScanner", "Unknown path \"" + path + "\" complete.");
+ invokeScanComplete();
+ }
+
+ private void invokeScanComplete() {
+ if (listener != null) {
+ if (fileToScan.isEmpty() && fileScanning.isEmpty())
+ listener.onScanComplete();
+ }
+ }
+
+ public synchronized void setScannerListener(ScannerListener listener) {
+ this.listener = listener;
+ invokeScanComplete();
+ }
+
+ public void close() {
+ scanner.disconnect();
+ }
+
+ public interface ScannerListener {
+
+ public void onScanComplete();
+
+ }
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Audio.Playlists;
+import android.provider.MediaStore.Audio.Media;
+import android.util.Log;
+import android.util.Pair;
+
+public class PlaylistUpdater implements Runnable {
+
+ private ContentResolver resolver;
+ private UpdaterListener listener;
+ private Map<String, List<String>> playlists;
+
+ private List<Pair<String, Long>> playlistList;
+
+ public PlaylistUpdater(Context context, UpdaterListener listener,
+ Map<String, List<String>> playlists) {
+ resolver = context.getContentResolver();
+ this.listener = listener;
+ this.playlists = playlists;
+ playlistList = new ArrayList<Pair<String, Long>>(playlists.size());
+ }
+
+ @Override
+ public void run() {
+ maintainListOfPlaylist();
+ for (Pair<String, Long> pl : playlistList)
+ updatePlaylist(pl.second, playlists.get(pl.first));
+ listener.onUpdateComplete();
+ }
+
+ protected void maintainListOfPlaylist() {
+ String[] cols = { Playlists._ID, Playlists.NAME };
+ Uri uri = Playlists.EXTERNAL_CONTENT_URI;
+ Cursor cursor = resolver.query(uri, cols, null, null, null);
+ assert cursor != null;
+ int idIndex = cursor.getColumnIndex(Playlists._ID);
+ int nameIndex = cursor.getColumnIndex(Playlists.NAME);
+
+ Set<String> plToCreate = new HashSet<String>(playlists.keySet());
+ List<String> plToDelete = new LinkedList<String>();
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idIndex);
+ String name = cursor.getString(nameIndex);
+ if (!playlists.containsKey(name)) {
+ plToDelete.add(String.valueOf(id));
+ } else {
+ plToCreate.remove(name);
+ playlistList.add(new Pair<String, Long>(name, id));
+ }
+ }
+ cursor.close();
+
+ for (String id : plToDelete)
+ resolver.delete(uri, Playlists._ID + "=" + id, null);
+
+ for (String name : plToCreate) {
+ ContentValues values = new ContentValues();
+ values.put(Playlists.NAME, name);
+ Uri rowUri = resolver.insert(uri, values);
+ long id = ContentUris.parseId(rowUri);
+ playlistList.add(new Pair<String, Long>(name, id));
+ }
+ }
+
+ protected long getAudioId(String fileId) {
+ Cursor cursor = resolver.query(
+ Media.EXTERNAL_CONTENT_URI,
+ new String[] { Media._ID },
+ Media.DATA + " like ?",
+ new String[] { "%" + fileId },
+ null);
+ assert cursor != null && cursor.getCount() == 1;
+ cursor.moveToFirst();
+ long result = cursor.getLong(cursor.getColumnIndex(Media._ID));
+ cursor.close();
+ return result;
+ }
+
+ protected void updatePlaylist(long playlistId, List<String> members) {
+ int total = members.size();
+ Map<Long, Long> memberMaps = new HashMap<Long, Long>(total);
+ for (int i = 0; i < total; i++) {
+ String fileId = members.get(i);
+ memberMaps.put(getAudioId(fileId), (long) i);
+ }
+
+ Uri uri = Playlists.Members.getContentUri("external", playlistId);
+ Cursor cursor = resolver.query(uri,
+ new String[] {
+ Playlists.Members.AUDIO_ID,
+ Playlists.Members.PLAY_ORDER },
+ null, null, null);
+ assert cursor != null;
+ int idIndex = cursor.getColumnIndex(Playlists.Members.AUDIO_ID);
+ int orderIndex = cursor.getColumnIndex(Playlists.Members.PLAY_ORDER);
+
+ Set<Long> audioToAdd = new HashSet<Long>(memberMaps.keySet());
+ List<String> audioToDelete = new LinkedList<String>();
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idIndex);
+ long order = cursor.getLong(orderIndex);
+ if (memberMaps.containsKey(id) && order == memberMaps.get(id)) {
+ audioToAdd.remove(id);
+ } else {
+ audioToDelete.add(String.valueOf(id));
+ }
+ }
+ cursor.close();
+
+ Log.d("Playlist", String.format("Total: %d, remove: %d, add: %d",
+ members.size(), audioToDelete.size(), audioToAdd.size()));
+
+ for (String id : audioToDelete)
+ resolver.delete(uri, Playlists.Members.AUDIO_ID + "=" + id, null);
+
+ int order = 0;
+ for (long audio : audioToAdd) {
+ ContentValues values = new ContentValues();
+ values.put(Playlists.Members.AUDIO_ID, audio);
+ values.put(Playlists.Members.PLAY_ORDER, order++);
+ resolver.insert(uri, values);
+ }
+ }
+
+ public interface UpdaterListener {
+
+ public void onUpdateComplete();
+
+ }
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.upsuper.playlistsyncer.client.R;
+
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdManager.DiscoveryListener;
+import android.net.nsd.NsdManager.ResolveListener;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Bundle;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class SelectActivity extends Activity
+ implements DiscoveryListener, OnItemClickListener, ResolveListener, OnCancelListener {
+
+ public static final String SERVICE_TYPE = "_PlaylistSyncer._tcp.";
+
+ private ArrayAdapter<String> adapter;
+ private ListView listService;
+ private ProgressDialog dialog;
+
+ private NsdManager nsdManager;
+ private Map<String, NsdServiceInfo> serviceMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_select);
+ listService = (ListView) findViewById(R.id.list_service);
+ adapter = new ArrayAdapter<String>(this, R.layout.view_service_item);
+ listService.setAdapter(adapter);
+ listService.setOnItemClickListener(this);
+
+ serviceMap = new HashMap<String, NsdServiceInfo>();
+ nsdManager = (NsdManager) getSystemService(Context.NSD_SERVICE);
+
+ dialog = new ProgressDialog(this);
+ dialog.setIndeterminate(true);
+ dialog.setOnCancelListener(this);
+ dialog.setMessage("Waiting...");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dialog.show();
+ nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, this);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ nsdManager.stopServiceDiscovery(this);
+ adapter.clear();
+ serviceMap.clear();
+ dialog.hide();
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ // TODO
+ Log.d("NSD", "onDiscoveryStarted: " + serviceType);
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ // TODO
+ Log.d("NSD", "onDiscoveryStopped: " + serviceType);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ final String serviceName = serviceInfo.getServiceName();
+ if (!serviceMap.containsKey(serviceName)) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.add(serviceName);
+ dialog.hide();
+ }
+ });
+ }
+ serviceMap.put(serviceName, serviceInfo);
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ final String serviceName = serviceInfo.getServiceName();
+ serviceMap.remove(serviceName);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.remove(serviceName);
+ if (serviceMap.size() == 0)
+ dialog.show();
+ }
+ });
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ // TODO
+ Log.d("NSD", "onStartDiscoveryFailed: " + serviceType + " (" + errorCode + ")");
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ // TODO
+ Log.d("NSD", "onStopDiscoveryFailed: " + serviceType + " (" + errorCode + ")");
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String serviceName = adapter.getItem(position);
+ NsdServiceInfo serviceInfo = serviceMap.get(serviceName);
+ nsdManager.resolveService(serviceInfo, this);
+ }
+
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Toast.makeText(this, "Resolve failed: " + errorCode, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ Intent intent = new Intent(this, SyncActivity.class);
+ intent.putExtra(SyncActivity.EXTRA_SERVICE_INFO, serviceInfo);
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import org.upsuper.playlistsyncer.client.MediaScannerClient.ScannerListener;
+import org.upsuper.playlistsyncer.client.PlaylistUpdater.UpdaterListener;
+import org.upsuper.playlistsyncer.client.SyncerClient.ClientListener;
+
+import android.net.nsd.NsdServiceInfo;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Toast;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Intent;
+
+import static org.upsuper.playlistsyncer.client.Constants.PACKAGE_NAME;
+
+public class SyncActivity extends Activity
+ implements ClientListener, ScannerListener, UpdaterListener {
+
+ public static final String EXTRA_SERVICE_INFO = PACKAGE_NAME + ".SERVICE_INFO";
+
+ SyncerClient client;
+ Thread thread;
+ List<String> downloadList;
+ Map<String, List<String>> playlists;
+
+ MediaScannerClient scanner;
+
+ private ProgressDialog dialog;
+ private ListView listFile;
+ private FileListAdapter adapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_sync);
+ listFile = (ListView) findViewById(R.id.list_file);
+
+ Intent intent = getIntent();
+ NsdServiceInfo service = (NsdServiceInfo)
+ intent.getParcelableExtra(EXTRA_SERVICE_INFO);
+ client = new SyncerClient(
+ service.getHost(), service.getPort(), this);
+
+ scanner = new MediaScannerClient(this);
+ thread = new Thread(client);
+ thread.start();
+ }
+
+ private void showProgressDialog(String msg) {
+ dialog = new ProgressDialog(this);
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
+ dialog.setMessage(msg);
+ dialog.show();
+ }
+
+ @Override
+ public void onConfirmCodeGenerated(final String code) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showProgressDialog("Waiting... Confirm code: " + code);
+ }
+ });
+ }
+
+ @Override
+ public void onDownloadListAvailable(final List<String> list) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ dialog.dismiss();
+ downloadList = list;
+
+ for (String fileId : list)
+ Log.d("DList", fileId);
+ adapter = new FileListAdapter(SyncActivity.this, list);
+ listFile.setAdapter(adapter);
+ }
+ });
+ }
+
+ @Override
+ public void onStartDownload(final int index, final int size) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.setCurrentPosition(index, size);
+ if (index >= listFile.getFirstVisiblePosition() &&
+ index <= listFile.getLastVisiblePosition()) {
+ View child = listFile.getChildAt(index);
+ int childHeight = child.getHeight();
+ int listHeight = listFile.getHeight();
+ int top = (listHeight - childHeight) / 2;
+ listFile.smoothScrollToPositionFromTop(index, top);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onDownloadProgress(final int index, final int received) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.updateProgress(received);
+ }
+ });
+ }
+
+ @Override
+ public void onFinishDownload(final int index, File file) {
+ scanner.scanFile(file);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ adapter.setCurrentPosition(index + 1, 0);
+ }
+ });
+ }
+
+ @Override
+ public void onPlaylistsReceived(Map<String, List<String>> playlists) {
+ this.playlists = playlists;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showProgressDialog("Updating playlists...");
+ // It is necessary to wait for scanning
+ // before updating playlists.
+ scanner.setScannerListener(SyncActivity.this);
+ }
+ });
+ }
+
+ @Override
+ public void onClientError(final Exception exception) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String msg = "Error occurs! " + exception.getMessage();
+ Toast.makeText(SyncActivity.this, msg, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onScanComplete() {
+ scanner.close();
+ PlaylistUpdater updater = new PlaylistUpdater(this, this, playlists);
+ new Thread(updater).start();
+ }
+
+ @Override
+ public void onUpdateComplete() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ dialog.dismiss();
+ String msg = "Sync successfully finished!!";
+ Toast.makeText(SyncActivity.this, msg, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ });
+ }
+
+}
--- /dev/null
+package org.upsuper.playlistsyncer.client;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import android.util.Log;
+import android.util.Pair;
+
+public class SyncerClient implements Runnable {
+
+ protected static final String PROTOCOL_NAME = "PLSY/1.0";
+ private static final int CONFIRM_CODE_LENGTH = 5;
+ private static final String CONFIRM_CODE_ALPHABET =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ private static final int BUFFER_SIZE = 4096;
+
+ protected static final List<String> AUDIO_EXTENSIONS =
+ Arrays.asList(new String[] {"mp3", "m4a"});
+
+ private InetAddress dstAddress;
+ private int dstPort;
+ private InputStream in;
+ private PrintStream out;
+ private byte[] buffer = new byte[BUFFER_SIZE];
+ MessageDigest digester;
+
+ private List<FileItem> fileList;
+ private List<FileItem> downloadList;
+
+ ClientListener listener;
+
+ public SyncerClient(InetAddress address, int port, ClientListener listener) {
+ dstAddress = address;
+ dstPort = port;
+ this.listener = listener;
+ try {
+ digester = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) { }
+ }
+
+ @Override
+ public void run() {
+ try {
+ Socket socket = new Socket(dstAddress, dstPort);
+ in = new BufferedInputStream(socket.getInputStream());
+ out = new PrintStream(socket.getOutputStream());
+
+ String confirmCode = generateConfirmCode();
+ listener.onConfirmCodeGenerated(confirmCode);
+ writeHeader(confirmCode);
+
+ readFileList();
+ removeUnusedFiles();
+ prepareDownloadList();
+ receiveFiles();
+ receivePlaylists();
+ } catch (IOException e) {
+ listener.onClientError(e);
+ }
+ }
+
+ protected String generateConfirmCode() {
+ Random rand = new Random();
+ char[] code = new char[CONFIRM_CODE_LENGTH];
+ for (int i = 0; i < CONFIRM_CODE_LENGTH; i++) {
+ int pos = rand.nextInt(CONFIRM_CODE_ALPHABET.length());
+ code[i] = CONFIRM_CODE_ALPHABET.charAt(pos);
+ }
+ return new String(code);
+ }
+
+ protected void writeHeader(String confirmCode) throws IOException {
+ out.println(PROTOCOL_NAME + " " + confirmCode);
+ if (out.checkError())
+ throw new IOException("writeHeader");
+ }
+
+ protected String readLine() throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
+ while (true) {
+ int b = in.read();
+ if (b == '\n')
+ break;
+ buffer.put((byte) b);
+ }
+ return new String(buffer.array(), 0, buffer.position(), "UTF-8");
+ }
+
+ protected Pair<String, String> splitLine(String line) throws IOException {
+ int seperator = line.lastIndexOf(' ');
+ return new Pair<String, String>(
+ line.substring(0, seperator),
+ line.substring(seperator + 1));
+ }
+
+ protected void readFileList() throws IOException {
+ fileList = new ArrayList<FileItem>();
+ while (true) {
+ String line = readLine();
+ if (line.isEmpty()) break;
+
+ Pair<String, String> splitLine = splitLine(line);
+ FileItem item = new FileItem();
+ item.fileId = splitLine.first;
+ item.size = Integer.valueOf(splitLine.second);
+ item.file = new File(Constants.MUSIC_DIRECTORY, item.fileId);
+ fileList.add(item);
+ }
+ }
+
+ protected void removeUnusedFiles(File file, Set<String> files) {
+ if (file.isDirectory()) {
+ for (File f : file.listFiles())
+ removeUnusedFiles(f, files);
+ } else {
+ String name = file.getName();
+ int dotpos = name.lastIndexOf('.');
+ if (dotpos >= 0) {
+ String ext = name.substring(dotpos + 1).toLowerCase();
+ if (!AUDIO_EXTENSIONS.contains(ext))
+ return;
+ if (!files.contains(file.getAbsolutePath())) {
+ file.delete();
+ Log.i("Client", "Removed: " + file.getAbsolutePath());
+ }
+ }
+ }
+ }
+
+ protected void removeUnusedFiles() {
+ Set<String> files = new HashSet<String>(fileList.size());
+ for (FileItem item : fileList)
+ files.add(item.file.getAbsolutePath());
+ removeUnusedFiles(Constants.MUSIC_DIRECTORY, files);
+ }
+
+ protected void prepareDownloadList() throws IOException {
+ downloadList = new LinkedList<FileItem>();
+ for (FileItem item : fileList) {
+ if (!item.file.exists() || item.file.length() != item.size)
+ downloadList.add(item);
+ }
+
+ List<String> list = new ArrayList<String>(downloadList.size());
+ for (FileItem item : downloadList)
+ list.add(item.fileId);
+ listener.onDownloadListAvailable(list);
+ }
+
+ protected void receiveFile(File file, int index, int size) throws IOException {
+ file.getParentFile().mkdirs();
+ FileOutputStream fileStream = new FileOutputStream(file);
+
+ try {
+ int received = 0;
+ while (received < size) {
+ int length = Math.min(BUFFER_SIZE, size - received);
+ int read = in.read(buffer, 0, length);
+ if (read == -1)
+ throw new IOException("receiveFile");
+ received += read;
+ fileStream.write(buffer, 0, read);
+ digester.update(buffer, 0, read);
+ listener.onDownloadProgress(index, received);
+ }
+
+ byte[] localDigest = digester.digest();
+ byte[] remoteDigest = new byte[localDigest.length];
+ in.read(remoteDigest, 0, remoteDigest.length);
+ if (!Arrays.equals(localDigest, remoteDigest))
+ throw new IOException("Wrong digest");
+ } catch (IOException e) {
+ file.delete();
+ throw e;
+ } finally {
+ fileStream.close();
+ }
+ }
+
+ protected void receiveFiles() throws IOException {
+ int size = downloadList.size();
+ for (int i = 0; i < size; i++) {
+ FileItem item = downloadList.get(i);
+ out.println(item.fileId);
+ if (out.checkError())
+ throw new IOException("sendDownloadList");
+
+ File file = new File(Constants.MUSIC_DIRECTORY, item.fileId);
+ listener.onStartDownload(i, item.size);
+ receiveFile(file, i, item.size);
+ listener.onFinishDownload(i, file);
+ }
+
+ out.println();
+ if (out.checkError())
+ throw new IOException("sendDownloadList");
+ }
+
+ protected void receivePlaylists() throws IOException {
+ Map<String, List<String>> playlists =
+ new LinkedHashMap<String, List<String>>();
+
+ while (true) {
+ String name = readLine();
+ if (name.isEmpty()) break;
+
+ List<String> playlist = new ArrayList<String>();
+ while (true) {
+ String line = readLine();
+ if (line.isEmpty()) break;
+ playlist.add(line);
+ }
+ playlists.put(name, playlist);
+ }
+
+ listener.onPlaylistsReceived(playlists);
+ }
+
+ private class FileItem {
+ public String fileId;
+ public int size;
+ public File file;
+ }
+
+ public interface ClientListener {
+
+ public void onConfirmCodeGenerated(String code);
+
+ public void onDownloadListAvailable(List<String> list);
+
+ public void onStartDownload(int index, int size);
+
+ public void onDownloadProgress(int index, int received);
+
+ public void onFinishDownload(int index, File file);
+
+ public void onPlaylistsReceived(Map<String, List<String>> playlists);
+
+ public void onClientError(Exception exception);
+
+ }
+
+}
--- /dev/null
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0B18599017B8E3AC00C98709 /* NSString+UTF8Data.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B18598F17B8E3AC00C98709 /* NSString+UTF8Data.m */; };
+ 0B4E4C6017ACC246005FCE9D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B4E4C5F17ACC246005FCE9D /* Security.framework */; };
+ 0B6C9091178C135800970498 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B6C9090178C135800970498 /* Cocoa.framework */; };
+ 0B6C909B178C135800970498 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0B6C9099178C135800970498 /* InfoPlist.strings */; };
+ 0B6C909D178C135800970498 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B6C909C178C135800970498 /* main.m */; };
+ 0B6C90A1178C135800970498 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 0B6C909F178C135800970498 /* Credits.rtf */; };
+ 0B6C90A4178C135800970498 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B6C90A3178C135800970498 /* AppDelegate.m */; };
+ 0B6C90A7178C135900970498 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B6C90A5178C135900970498 /* MainMenu.xib */; };
+ 0B8E48AC17B672DB008F48B7 /* NSURL+relativePath.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B8E48AB17B672DB008F48B7 /* NSURL+relativePath.m */; };
+ 0BC77AE9178DA6F40040D00B /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BC77AE6178DA6F40040D00B /* GCDAsyncSocket.m */; };
+ 0BC77AEA178DA6F40040D00B /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BC77AE8178DA6F40040D00B /* GCDAsyncUdpSocket.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 0B18598E17B8E3AC00C98709 /* NSString+UTF8Data.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+UTF8Data.h"; sourceTree = "<group>"; };
+ 0B18598F17B8E3AC00C98709 /* NSString+UTF8Data.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+UTF8Data.m"; sourceTree = "<group>"; };
+ 0B4E4C5F17ACC246005FCE9D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
+ 0B6C908D178C135800970498 /* SyncerServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SyncerServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0B6C9090178C135800970498 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+ 0B6C9093178C135800970498 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
+ 0B6C9094178C135800970498 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
+ 0B6C9095178C135800970498 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ 0B6C9098178C135800970498 /* SyncerServer-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SyncerServer-Info.plist"; sourceTree = "<group>"; };
+ 0B6C909A178C135800970498 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 0B6C909C178C135800970498 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 0B6C909E178C135800970498 /* SyncerServer-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SyncerServer-Prefix.pch"; sourceTree = "<group>"; };
+ 0B6C90A0178C135800970498 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = "<group>"; };
+ 0B6C90A2178C135800970498 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 0B6C90A3178C135800970498 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 0B6C90A6178C135900970498 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = "<group>"; };
+ 0B8E48AA17B672DB008F48B7 /* NSURL+relativePath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURL+relativePath.h"; sourceTree = "<group>"; };
+ 0B8E48AB17B672DB008F48B7 /* NSURL+relativePath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURL+relativePath.m"; sourceTree = "<group>"; };
+ 0BC77AE5178DA6F40040D00B /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; };
+ 0BC77AE6178DA6F40040D00B /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = "<group>"; };
+ 0BC77AE7178DA6F40040D00B /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
+ 0BC77AE8178DA6F40040D00B /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncUdpSocket.m; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0B6C908A178C135800970498 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0B4E4C6017ACC246005FCE9D /* Security.framework in Frameworks */,
+ 0B6C9091178C135800970498 /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0B6C9084178C135800970498 = {
+ isa = PBXGroup;
+ children = (
+ 0B6C9096178C135800970498 /* SyncerServer */,
+ 0B6C908F178C135800970498 /* Frameworks */,
+ 0B6C908E178C135800970498 /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 0B6C908E178C135800970498 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 0B6C908D178C135800970498 /* SyncerServer.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 0B6C908F178C135800970498 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0B4E4C5F17ACC246005FCE9D /* Security.framework */,
+ 0B6C9090178C135800970498 /* Cocoa.framework */,
+ 0B6C9092178C135800970498 /* Other Frameworks */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 0B6C9092178C135800970498 /* Other Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0B6C9093178C135800970498 /* AppKit.framework */,
+ 0B6C9094178C135800970498 /* CoreData.framework */,
+ 0B6C9095178C135800970498 /* Foundation.framework */,
+ );
+ name = "Other Frameworks";
+ sourceTree = "<group>";
+ };
+ 0B6C9096178C135800970498 /* SyncerServer */ = {
+ isa = PBXGroup;
+ children = (
+ 0B6C90A2178C135800970498 /* AppDelegate.h */,
+ 0B6C90A3178C135800970498 /* AppDelegate.m */,
+ 0B18598E17B8E3AC00C98709 /* NSString+UTF8Data.h */,
+ 0B18598F17B8E3AC00C98709 /* NSString+UTF8Data.m */,
+ 0B8E48AA17B672DB008F48B7 /* NSURL+relativePath.h */,
+ 0B8E48AB17B672DB008F48B7 /* NSURL+relativePath.m */,
+ 0B6C90A5178C135900970498 /* MainMenu.xib */,
+ 0BC77AE4178DA6C60040D00B /* Libraries */,
+ 0B6C9097178C135800970498 /* Supporting Files */,
+ );
+ path = SyncerServer;
+ sourceTree = "<group>";
+ };
+ 0B6C9097178C135800970498 /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 0B6C9098178C135800970498 /* SyncerServer-Info.plist */,
+ 0B6C9099178C135800970498 /* InfoPlist.strings */,
+ 0B6C909C178C135800970498 /* main.m */,
+ 0B6C909E178C135800970498 /* SyncerServer-Prefix.pch */,
+ 0B6C909F178C135800970498 /* Credits.rtf */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ 0BC77AE4178DA6C60040D00B /* Libraries */ = {
+ isa = PBXGroup;
+ children = (
+ 0BC77AE5178DA6F40040D00B /* GCDAsyncSocket.h */,
+ 0BC77AE6178DA6F40040D00B /* GCDAsyncSocket.m */,
+ 0BC77AE7178DA6F40040D00B /* GCDAsyncUdpSocket.h */,
+ 0BC77AE8178DA6F40040D00B /* GCDAsyncUdpSocket.m */,
+ );
+ name = Libraries;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 0B6C908C178C135800970498 /* SyncerServer */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0B6C90AA178C135900970498 /* Build configuration list for PBXNativeTarget "SyncerServer" */;
+ buildPhases = (
+ 0B6C9089178C135800970498 /* Sources */,
+ 0B6C908A178C135800970498 /* Frameworks */,
+ 0B6C908B178C135800970498 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = SyncerServer;
+ productName = SyncerServer;
+ productReference = 0B6C908D178C135800970498 /* SyncerServer.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 0B6C9085178C135800970498 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0460;
+ ORGANIZATIONNAME = "Xidorn Quan";
+ };
+ buildConfigurationList = 0B6C9088178C135800970498 /* Build configuration list for PBXProject "SyncerServer" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ );
+ mainGroup = 0B6C9084178C135800970498;
+ productRefGroup = 0B6C908E178C135800970498 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 0B6C908C178C135800970498 /* SyncerServer */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 0B6C908B178C135800970498 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0B6C909B178C135800970498 /* InfoPlist.strings in Resources */,
+ 0B6C90A1178C135800970498 /* Credits.rtf in Resources */,
+ 0B6C90A7178C135900970498 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 0B6C9089178C135800970498 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0B6C909D178C135800970498 /* main.m in Sources */,
+ 0B6C90A4178C135800970498 /* AppDelegate.m in Sources */,
+ 0BC77AE9178DA6F40040D00B /* GCDAsyncSocket.m in Sources */,
+ 0BC77AEA178DA6F40040D00B /* GCDAsyncUdpSocket.m in Sources */,
+ 0B8E48AC17B672DB008F48B7 /* NSURL+relativePath.m in Sources */,
+ 0B18599017B8E3AC00C98709 /* NSString+UTF8Data.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 0B6C9099178C135800970498 /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0B6C909A178C135800970498 /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 0B6C909F178C135800970498 /* Credits.rtf */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0B6C90A0178C135800970498 /* en */,
+ );
+ name = Credits.rtf;
+ sourceTree = "<group>";
+ };
+ 0B6C90A5178C135900970498 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0B6C90A6178C135900970498 /* en */,
+ );
+ name = MainMenu.xib;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 0B6C90A8178C135900970498 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.8;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ };
+ name = Debug;
+ };
+ 0B6C90A9178C135900970498 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.8;
+ SDKROOT = macosx;
+ };
+ name = Release;
+ };
+ 0B6C90AB178C135900970498 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COMBINE_HIDPI_IMAGES = YES;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "SyncerServer/SyncerServer-Prefix.pch";
+ INFOPLIST_FILE = "SyncerServer/SyncerServer-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Debug;
+ };
+ 0B6C90AC178C135900970498 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COMBINE_HIDPI_IMAGES = YES;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "SyncerServer/SyncerServer-Prefix.pch";
+ INFOPLIST_FILE = "SyncerServer/SyncerServer-Info.plist";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 0B6C9088178C135800970498 /* Build configuration list for PBXProject "SyncerServer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0B6C90A8178C135900970498 /* Debug */,
+ 0B6C90A9178C135900970498 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0B6C90AA178C135900970498 /* Build configuration list for PBXNativeTarget "SyncerServer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0B6C90AB178C135900970498 /* Debug */,
+ 0B6C90AC178C135900970498 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 0B6C9085178C135800970498 /* Project object */;
+}
--- /dev/null
+//
+// AppDelegate.h
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-7-9.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+#import "GCDAsyncSocket.h"
+
+@interface AppDelegate : NSObject <
+ NSApplicationDelegate, NSTableViewDataSource,
+ NSNetServiceDelegate, GCDAsyncSocketDelegate>
+{
+ NSNetService *netService;
+ GCDAsyncSocket *listenSocket;
+ GCDAsyncSocket *transferSocket;
+
+ NSMutableArray *trackList;
+ NSMutableDictionary *trackFiles;
+ NSMutableDictionary *playlistData;
+}
+
+@property (assign) IBOutlet NSWindow *window;
+@property (weak) IBOutlet NSTableView *trackTable;
+@property (weak) IBOutlet NSProgressIndicator *progressIndicator;
+@property (weak) IBOutlet NSButton *startButton;
+@property (weak) IBOutlet NSButton *retryButton;
+@property (weak) IBOutlet NSTextField *statusLabel;
+
+- (IBAction)doRetry:(id)sender;
+- (IBAction)startSync:(id)sender;
+
+@end
--- /dev/null
+//
+// AppDelegate.m
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-7-9.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import "AppDelegate.h"
+#import "NSURL+relativePath.h"
+#import "NSString+UTF8Data.h"
+#import <CommonCrypto/CommonCrypto.h>
+#include <math.h>
+
+enum {
+ kTagEstablish,
+ kTagFileList,
+ kTagDownloadRequest,
+ kTagSendFile,
+ kTagSendPlaylist,
+};
+
+NSData *kNewLine;
+
+NSString *humanReadableBytes(NSUInteger bytes, BOOL si)
+{
+ int unit = si ? 1000 : 1024;
+ if (bytes < unit)
+ return [NSString stringWithFormat:@"%lu B", bytes];
+ int exp = (int)(log(bytes) / log(unit));
+ char pre = (si ? "kMGTPE" : "KMGTPE")[exp - 1];
+ NSString *format = si ? @"%.2f %ciB" : @"%.2f %cB";
+ return [NSString stringWithFormat:format, bytes / pow(unit, exp), pre];
+}
+
+NSString *trimString(NSString *string)
+{
+ return [string stringByTrimmingCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+}
+
+@implementation AppDelegate
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+ NSString *libraryPath = [NSHomeDirectory()
+ stringByAppendingPathComponent:@"Music/iTunes/iTunes Music Library.xml"];
+ NSURL *mediaDir = [NSURL fileURLWithPath:
+ [NSHomeDirectory() stringByAppendingPathComponent:@"Music/iTunes/iTunes Media/Music"]];
+ NSDictionary *musicLibrary = [NSDictionary dictionaryWithContentsOfFile:libraryPath];
+
+ kNewLine = [GCDAsyncSocket LFData];
+
+ NSDictionary *tracks = musicLibrary[@"Tracks"];
+ NSArray *playlists = [musicLibrary[@"Playlists"] filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(id playlist, NSDictionary *bindings) {
+ if (playlist[@"Visible"] != nil)
+ return FALSE;
+ if (playlist[@"Distinguished Kind"] != nil)
+ return FALSE;
+ return TRUE;
+ }]];
+
+ trackList = [NSMutableArray array];
+ trackFiles = [NSMutableDictionary dictionary];
+ playlistData = [NSMutableDictionary dictionaryWithCapacity:[playlists count]];
+ for (id playlist in playlists) {
+ NSArray *items = playlist[@"Playlist Items"];
+ NSMutableArray *itemList = [NSMutableArray arrayWithCapacity:[items count]];
+ playlistData[playlist[@"Name"]] = itemList;
+
+ for (id item in items) {
+ id trackID = item[@"Track ID"];
+ trackID = [trackID stringValue];
+ id track = tracks[trackID];
+ NSURL *location = [NSURL URLWithString:track[@"Location"]];
+ NSString *fileId = [location relativePath:mediaDir];
+ [itemList addObject:fileId];
+
+ if ([trackFiles objectForKey:fileId] == nil) {
+ id itemData = @{
+ @"Track ID": trackID,
+ @"Name": track[@"Name"],
+ @"Size": track[@"Size"],
+ @"File ID": fileId,
+ @"Location": location
+ };
+ [trackList addObject:itemData];
+ trackFiles[fileId] = itemData;
+ }
+ }
+ }
+ [self.trackTable reloadData];
+
+ listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
+ [self publishService];
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
+ return YES;
+}
+
+- (void)publishService
+{
+ [transferSocket disconnect];
+ [self.statusLabel setStringValue:@"Waiting..."];
+ [self.startButton setEnabled:NO];
+ [self.retryButton setEnabled:NO];
+ [self.progressIndicator startAnimation:self];
+
+ BOOL ret = [listenSocket acceptOnPort:0 error:nil];
+ assert(ret);
+
+ UInt16 port = [listenSocket localPort];
+ netService = [[NSNetService alloc] initWithDomain:@"local."
+ type:@"_PlaylistSyncer._tcp."
+ name:@""
+ port:port];
+ [netService setDelegate:self];
+ [netService publish];
+}
+
+- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
+{
+ [netService stop];
+ [sock disconnect];
+ [self.retryButton setEnabled:YES];
+
+ transferSocket = newSocket;
+ [transferSocket readDataToData:kNewLine withTimeout:-1 tag:kTagEstablish];
+}
+
+- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
+{
+ switch (tag) {
+ case kTagEstablish: {
+ NSString *confirmInfo = [NSString stringWithUTF8Data:data];
+ confirmInfo = trimString(confirmInfo);
+ [self.statusLabel setStringValue:[NSString stringWithFormat:@"Connected: %@", confirmInfo]];
+ [self.progressIndicator stopAnimation:self];
+ [self.startButton setEnabled:YES];
+ break;
+ }
+
+ case kTagDownloadRequest: {
+ NSString *fileId = [NSString stringWithUTF8Data:data];
+ fileId = trimString(fileId);
+ if (![fileId isEqualToString:@""]) {
+ id item = trackFiles[fileId];
+ NSData *fileData = [NSData dataWithContentsOfURL:item[@"Location"]];
+ if ([fileData length] != [item[@"Size"] unsignedLongValue]) {
+ [NSApp terminate:self];
+ }
+
+ NSLog(@"Sending file: %@ (size: %lu)", fileId, [fileData length]);
+ [transferSocket writeData:fileData withTimeout:-1 tag:kTagSendFile];
+
+ unsigned char md5[16];
+ CC_MD5([fileData bytes], (CC_LONG) [fileData length], md5);
+ [transferSocket writeData:[NSData dataWithBytes:md5 length:sizeof(md5)]
+ withTimeout:-1 tag:kTagSendFile];
+
+ [transferSocket readDataToData:kNewLine withTimeout:-1 tag:kTagDownloadRequest];
+ } else {
+ // send playlist
+ for (NSString *name in playlistData) {
+ NSString *nameData = [NSString stringWithFormat:@"%@\n", name];
+ [transferSocket writeData:[nameData UTF8Data] withTimeout:-1 tag:kTagSendPlaylist];
+ for (NSString *item in playlistData[name]) {
+ NSString *fileId = [NSString stringWithFormat:@"%@\n", item];
+ [transferSocket writeData:[fileId UTF8Data] withTimeout:-1 tag:kTagSendPlaylist];
+ }
+ [transferSocket writeData:kNewLine withTimeout:-1 tag:kTagSendPlaylist];
+ }
+ [transferSocket writeData:kNewLine withTimeout:-1 tag:kTagSendPlaylist];
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
+{
+ return [trackList count];
+}
+
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn
+ row:(NSInteger)rowIndex
+{
+ NSString *identifier = [aTableColumn identifier];
+ id data = trackList[rowIndex];
+ if ([identifier isEqualToString:@"status"]) {
+ return [NSImage imageNamed:NSImageNameStatusNone];
+ } else if ([identifier isEqualToString:@"name"]) {
+ return data[@"Name"];
+ } else if ([identifier isEqualToString:@"size"]) {
+ return humanReadableBytes([data[@"Size"] unsignedLongValue], NO);
+ } else if ([identifier isEqualToString:@"file"]) {
+ return data[@"File ID"];
+ }
+ return nil;
+}
+
+- (IBAction)doRetry:(id)sender {
+ [self publishService];
+}
+
+- (IBAction)startSync:(id)sender {
+ for (id item in trackList) {
+ NSString *data = [NSString stringWithFormat:@"%@ %lu\n", item[@"File ID"], [item[@"Size"] unsignedLongValue]];
+ [transferSocket writeData:[data UTF8Data] withTimeout:-1 tag:kTagFileList];
+ }
+ [transferSocket writeData:kNewLine withTimeout:-1 tag:kTagFileList];
+ [transferSocket readDataToData:kNewLine withTimeout:-1 tag:kTagDownloadRequest];
+}
+
+@end
--- /dev/null
+//
+// GCDAsyncSocket.h
+//
+// This class is in the public domain.
+// Originally created by Robbie Hanson in Q3 2010.
+// Updated and maintained by Deusty LLC and the Apple development community.
+//
+// https://github.com/robbiehanson/CocoaAsyncSocket
+//
+
+#import <Foundation/Foundation.h>
+#import <Security/Security.h>
+#import <Security/SecureTransport.h>
+#import <dispatch/dispatch.h>
+
+@class GCDAsyncReadPacket;
+@class GCDAsyncWritePacket;
+@class GCDAsyncSocketPreBuffer;
+
+#if TARGET_OS_IPHONE
+
+ // Compiling for iOS
+
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000 // iOS 5.0 supported
+
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 // iOS 5.0 supported and required
+
+ #define IS_SECURE_TRANSPORT_AVAILABLE YES
+ #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1
+ #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 0
+
+ #else // iOS 5.0 supported but not required
+
+ #ifndef NSFoundationVersionNumber_iPhoneOS_5_0
+ #define NSFoundationVersionNumber_iPhoneOS_5_0 881.00
+ #endif
+
+ #define IS_SECURE_TRANSPORT_AVAILABLE (NSFoundationVersionNumber >= NSFoundationVersionNumber_iPhoneOS_5_0)
+ #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1
+ #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 1
+
+ #endif
+
+ #else // iOS 5.0 not supported
+
+ #define IS_SECURE_TRANSPORT_AVAILABLE NO
+ #define SECURE_TRANSPORT_MAYBE_AVAILABLE 0
+ #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 1
+
+ #endif
+
+#else
+
+ // Compiling for Mac OS X
+
+ #define IS_SECURE_TRANSPORT_AVAILABLE YES
+ #define SECURE_TRANSPORT_MAYBE_AVAILABLE 1
+ #define SECURE_TRANSPORT_MAYBE_UNAVAILABLE 0
+
+#endif
+
+extern NSString *const GCDAsyncSocketException;
+extern NSString *const GCDAsyncSocketErrorDomain;
+
+extern NSString *const GCDAsyncSocketQueueName;
+extern NSString *const GCDAsyncSocketThreadName;
+
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+extern NSString *const GCDAsyncSocketSSLCipherSuites;
+#if TARGET_OS_IPHONE
+extern NSString *const GCDAsyncSocketSSLProtocolVersionMin;
+extern NSString *const GCDAsyncSocketSSLProtocolVersionMax;
+#else
+extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters;
+#endif
+#endif
+
+enum GCDAsyncSocketError
+{
+ GCDAsyncSocketNoError = 0, // Never used
+ GCDAsyncSocketBadConfigError, // Invalid configuration
+ GCDAsyncSocketBadParamError, // Invalid parameter was passed
+ GCDAsyncSocketConnectTimeoutError, // A connect operation timed out
+ GCDAsyncSocketReadTimeoutError, // A read operation timed out
+ GCDAsyncSocketWriteTimeoutError, // A write operation timed out
+ GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing
+ GCDAsyncSocketClosedError, // The remote peer closed the connection
+ GCDAsyncSocketOtherError, // Description provided in userInfo
+};
+typedef enum GCDAsyncSocketError GCDAsyncSocketError;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@interface GCDAsyncSocket : NSObject
+
+/**
+ * GCDAsyncSocket uses the standard delegate paradigm,
+ * but executes all delegate callbacks on a given delegate dispatch queue.
+ * This allows for maximum concurrency, while at the same time providing easy thread safety.
+ *
+ * You MUST set a delegate AND delegate dispatch queue before attempting to
+ * use the socket, or you will get an error.
+ *
+ * The socket queue is optional.
+ * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue.
+ * If you choose to provide a socket queue, the socket queue must not be a concurrent queue.
+ * If you choose to provide a socket queue, and the socket queue has a configured target queue,
+ * then please see the discussion for the method markSocketQueueTargetQueue.
+ *
+ * The delegate queue and socket queue can optionally be the same.
+**/
+- (id)init;
+- (id)initWithSocketQueue:(dispatch_queue_t)sq;
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq;
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq;
+
+#pragma mark Configuration
+
+- (id)delegate;
+- (void)setDelegate:(id)delegate;
+- (void)synchronouslySetDelegate:(id)delegate;
+
+- (dispatch_queue_t)delegateQueue;
+- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue;
+- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue;
+
+- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr;
+- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
+- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
+
+/**
+ * By default, both IPv4 and IPv6 are enabled.
+ *
+ * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols,
+ * and can simulataneously accept incoming connections on either protocol.
+ *
+ * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol.
+ * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4.
+ * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6.
+ * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen.
+ * By default, the preferred protocol is IPv4, but may be configured as desired.
+**/
+- (BOOL)isIPv4Enabled;
+- (void)setIPv4Enabled:(BOOL)flag;
+
+- (BOOL)isIPv6Enabled;
+- (void)setIPv6Enabled:(BOOL)flag;
+
+- (BOOL)isIPv4PreferredOverIPv6;
+- (void)setPreferIPv4OverIPv6:(BOOL)flag;
+
+/**
+ * User data allows you to associate arbitrary information with the socket.
+ * This data is not used internally by socket in any way.
+**/
+- (id)userData;
+- (void)setUserData:(id)arbitraryUserData;
+
+#pragma mark Accepting
+
+/**
+ * Tells the socket to begin listening and accepting connections on the given port.
+ * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it,
+ * and the socket:didAcceptNewSocket: delegate method will be invoked.
+ *
+ * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc)
+**/
+- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr;
+
+/**
+ * This method is the same as acceptOnPort:error: with the
+ * additional option of specifying which interface to listen on.
+ *
+ * For example, you could specify that the socket should only accept connections over ethernet,
+ * and not other interfaces such as wifi.
+ *
+ * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34").
+ * You may also use the special strings "localhost" or "loopback" to specify that
+ * the socket only accept connections from the local machine.
+ *
+ * You can see the list of interfaces via the command line utility "ifconfig",
+ * or programmatically via the getifaddrs() function.
+ *
+ * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method.
+**/
+- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr;
+
+#pragma mark Connecting
+
+/**
+ * Connects to the given host and port.
+ *
+ * This method invokes connectToHost:onPort:viaInterface:withTimeout:error:
+ * and uses the default interface, and no timeout.
+**/
+- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
+
+/**
+ * Connects to the given host and port with an optional timeout.
+ *
+ * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface.
+**/
+- (BOOL)connectToHost:(NSString *)host
+ onPort:(uint16_t)port
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr;
+
+/**
+ * Connects to the given host & port, via the optional interface, with an optional timeout.
+ *
+ * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2").
+ * The host may also be the special strings "localhost" or "loopback" to specify connecting
+ * to a service on the local machine.
+ *
+ * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
+ * The interface may also be used to specify the local port (see below).
+ *
+ * To not time out use a negative time interval.
+ *
+ * This method will return NO if an error is detected, and set the error pointer (if one was given).
+ * Possible errors would be a nil host, invalid interface, or socket is already connected.
+ *
+ * If no errors are detected, this method will start a background connect operation and immediately return YES.
+ * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable.
+ *
+ * Since this class supports queued reads and writes, you can immediately start reading and/or writing.
+ * All read/write operations will be queued, and upon socket connection,
+ * the operations will be dequeued and processed in order.
+ *
+ * The interface may optionally contain a port number at the end of the string, separated by a colon.
+ * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end)
+ * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424".
+ * To specify only local port: ":8082".
+ * Please note this is an advanced feature, and is somewhat hidden on purpose.
+ * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection.
+ * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere.
+ * Local ports do NOT need to match remote ports. In fact, they almost never do.
+ * This feature is here for networking professionals using very advanced techniques.
+**/
+- (BOOL)connectToHost:(NSString *)host
+ onPort:(uint16_t)port
+ viaInterface:(NSString *)interface
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr;
+
+/**
+ * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object.
+ * For example, a NSData object returned from NSNetService's addresses method.
+ *
+ * If you have an existing struct sockaddr you can convert it to a NSData object like so:
+ * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
+ * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
+ *
+ * This method invokes connectToAdd
+**/
+- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr;
+
+/**
+ * This method is the same as connectToAddress:error: with an additional timeout option.
+ * To not time out use a negative time interval, or simply use the connectToAddress:error: method.
+**/
+- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
+
+/**
+ * Connects to the given address, using the specified interface and timeout.
+ *
+ * The address is specified as a sockaddr structure wrapped in a NSData object.
+ * For example, a NSData object returned from NSNetService's addresses method.
+ *
+ * If you have an existing struct sockaddr you can convert it to a NSData object like so:
+ * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
+ * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
+ *
+ * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
+ * The interface may also be used to specify the local port (see below).
+ *
+ * The timeout is optional. To not time out use a negative time interval.
+ *
+ * This method will return NO if an error is detected, and set the error pointer (if one was given).
+ * Possible errors would be a nil host, invalid interface, or socket is already connected.
+ *
+ * If no errors are detected, this method will start a background connect operation and immediately return YES.
+ * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable.
+ *
+ * Since this class supports queued reads and writes, you can immediately start reading and/or writing.
+ * All read/write operations will be queued, and upon socket connection,
+ * the operations will be dequeued and processed in order.
+ *
+ * The interface may optionally contain a port number at the end of the string, separated by a colon.
+ * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end)
+ * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424".
+ * To specify only local port: ":8082".
+ * Please note this is an advanced feature, and is somewhat hidden on purpose.
+ * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection.
+ * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere.
+ * Local ports do NOT need to match remote ports. In fact, they almost never do.
+ * This feature is here for networking professionals using very advanced techniques.
+**/
+- (BOOL)connectToAddress:(NSData *)remoteAddr
+ viaInterface:(NSString *)interface
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr;
+
+#pragma mark Disconnecting
+
+/**
+ * Disconnects immediately (synchronously). Any pending reads or writes are dropped.
+ *
+ * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method
+ * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods).
+ * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns.
+ *
+ * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method)
+ * [asyncSocket setDelegate:nil];
+ * [asyncSocket disconnect];
+ * [asyncSocket release];
+ *
+ * If you plan on disconnecting the socket, and then immediately asking it to connect again,
+ * you'll likely want to do so like this:
+ * [asyncSocket setDelegate:nil];
+ * [asyncSocket disconnect];
+ * [asyncSocket setDelegate:self];
+ * [asyncSocket connect...];
+**/
+- (void)disconnect;
+
+/**
+ * Disconnects after all pending reads have completed.
+ * After calling this, the read and write methods will do nothing.
+ * The socket will disconnect even if there are still pending writes.
+**/
+- (void)disconnectAfterReading;
+
+/**
+ * Disconnects after all pending writes have completed.
+ * After calling this, the read and write methods will do nothing.
+ * The socket will disconnect even if there are still pending reads.
+**/
+- (void)disconnectAfterWriting;
+
+/**
+ * Disconnects after all pending reads and writes have completed.
+ * After calling this, the read and write methods will do nothing.
+**/
+- (void)disconnectAfterReadingAndWriting;
+
+#pragma mark Diagnostics
+
+/**
+ * Returns whether the socket is disconnected or connected.
+ *
+ * A disconnected socket may be recycled.
+ * That is, it can used again for connecting or listening.
+ *
+ * If a socket is in the process of connecting, it may be neither disconnected nor connected.
+**/
+- (BOOL)isDisconnected;
+- (BOOL)isConnected;
+
+/**
+ * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected.
+ * The host will be an IP address.
+**/
+- (NSString *)connectedHost;
+- (uint16_t)connectedPort;
+
+- (NSString *)localHost;
+- (uint16_t)localPort;
+
+/**
+ * Returns the local or remote address to which this socket is connected,
+ * specified as a sockaddr structure wrapped in a NSData object.
+ *
+ * See also the connectedHost, connectedPort, localHost and localPort methods.
+**/
+- (NSData *)connectedAddress;
+- (NSData *)localAddress;
+
+/**
+ * Returns whether the socket is IPv4 or IPv6.
+ * An accepting socket may be both.
+**/
+- (BOOL)isIPv4;
+- (BOOL)isIPv6;
+
+/**
+ * Returns whether or not the socket has been secured via SSL/TLS.
+ *
+ * See also the startTLS method.
+**/
+- (BOOL)isSecure;
+
+#pragma mark Reading
+
+// The readData and writeData methods won't block (they are asynchronous).
+//
+// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue.
+// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue.
+//
+// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.)
+// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method
+// is called to optionally allow you to extend the timeout.
+// Upon a timeout, the "socket:didDisconnectWithError:" method is called
+//
+// The tag is for your convenience.
+// You can use it as an array index, step number, state id, pointer, etc.
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, the socket will create a buffer for you.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while the socket is using it.
+ * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer via
+ * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO].
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ * A maximum of length bytes will be read.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ * If maxLength is zero, no length restriction is enforced.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while the socket is using it.
+ * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer via
+ * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO].
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)length
+ tag:(long)tag;
+
+/**
+ * Reads the given number of bytes.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * If the length is 0, this method does nothing and the delegate is not called.
+**/
+- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Reads the given number of bytes.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If the length is 0, this method does nothing and the delegate is not called.
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer via
+ * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO].
+**/
+- (void)readDataToLength:(NSUInteger)length
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * If you pass nil or zero-length data as the "data" parameter,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ *
+ * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter.
+ * If you're developing your own custom protocol, be sure your separator can not occur naturally as
+ * part of the data between separators.
+ * For example, imagine you want to send several small documents over a socket.
+ * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents.
+ * In this particular example, it would be better to use a protocol similar to HTTP with
+ * a header that includes the length of the document.
+ * Also be careful that your separator cannot occur naturally as part of the encoding for a character.
+ *
+ * The given data (separator) parameter should be immutable.
+ * For performance reasons, the socket will retain it, not copy it.
+ * So if it is immutable, don't modify it while the socket is using it.
+**/
+- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while the socket is using it.
+ * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer via
+ * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO].
+ *
+ * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter.
+ * If you're developing your own custom protocol, be sure your separator can not occur naturally as
+ * part of the data between separators.
+ * For example, imagine you want to send several small documents over a socket.
+ * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents.
+ * In this particular example, it would be better to use a protocol similar to HTTP with
+ * a header that includes the length of the document.
+ * Also be careful that your separator cannot occur naturally as part of the encoding for a character.
+ *
+ * The given data (separator) parameter should be immutable.
+ * For performance reasons, the socket will retain it, not copy it.
+ * So if it is immutable, don't modify it while the socket is using it.
+**/
+- (void)readDataToData:(NSData *)data
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * If maxLength is zero, no length restriction is enforced.
+ * Otherwise if maxLength bytes are read without completing the read,
+ * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError.
+ * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end.
+ *
+ * If you pass nil or zero-length data as the "data" parameter,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ * If you pass a maxLength parameter that is less than the length of the data parameter,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ *
+ * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter.
+ * If you're developing your own custom protocol, be sure your separator can not occur naturally as
+ * part of the data between separators.
+ * For example, imagine you want to send several small documents over a socket.
+ * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents.
+ * In this particular example, it would be better to use a protocol similar to HTTP with
+ * a header that includes the length of the document.
+ * Also be careful that your separator cannot occur naturally as part of the encoding for a character.
+ *
+ * The given data (separator) parameter should be immutable.
+ * For performance reasons, the socket will retain it, not copy it.
+ * So if it is immutable, don't modify it while the socket is using it.
+**/
+- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If maxLength is zero, no length restriction is enforced.
+ * Otherwise if maxLength bytes are read without completing the read,
+ * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError.
+ * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end.
+ *
+ * If you pass a maxLength parameter that is less than the length of the data (separator) parameter,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing (except maybe print a warning), and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while the socket is using it.
+ * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer via
+ * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO].
+ *
+ * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter.
+ * If you're developing your own custom protocol, be sure your separator can not occur naturally as
+ * part of the data between separators.
+ * For example, imagine you want to send several small documents over a socket.
+ * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents.
+ * In this particular example, it would be better to use a protocol similar to HTTP with
+ * a header that includes the length of the document.
+ * Also be careful that your separator cannot occur naturally as part of the encoding for a character.
+ *
+ * The given data (separator) parameter should be immutable.
+ * For performance reasons, the socket will retain it, not copy it.
+ * So if it is immutable, don't modify it while the socket is using it.
+**/
+- (void)readDataToData:(NSData *)data
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)length
+ tag:(long)tag;
+
+/**
+ * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check).
+ * The parameters "tag", "done" and "total" will be filled in if they aren't NULL.
+**/
+- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr;
+
+#pragma mark Writing
+
+/**
+ * Writes data to the socket, and calls the delegate when finished.
+ *
+ * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called.
+ * If the timeout value is negative, the write operation will not use a timeout.
+ *
+ * Thread-Safety Note:
+ * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
+ * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method
+ * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed.
+ * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it.
+ * This is for performance reasons. Often times, if NSMutableData is passed, it is because
+ * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead.
+ * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
+ * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time
+ * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
+**/
+- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check).
+ * The parameters "tag", "done" and "total" will be filled in if they aren't NULL.
+**/
+- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr;
+
+#pragma mark Security
+
+/**
+ * Secures the connection using SSL/TLS.
+ *
+ * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes
+ * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing
+ * the upgrade to TLS at the same time, without having to wait for the write to finish.
+ * Any reads or writes scheduled after this method is called will occur over the secured connection.
+ *
+ * The possible keys and values for the TLS settings are well documented.
+ * Standard keys are:
+ *
+ * - kCFStreamSSLLevel
+ * - kCFStreamSSLAllowsExpiredCertificates
+ * - kCFStreamSSLAllowsExpiredRoots
+ * - kCFStreamSSLAllowsAnyRoot
+ * - kCFStreamSSLValidatesCertificateChain
+ * - kCFStreamSSLPeerName
+ * - kCFStreamSSLCertificates
+ * - kCFStreamSSLIsServer
+ *
+ * If SecureTransport is available on iOS:
+ *
+ * - GCDAsyncSocketSSLCipherSuites
+ * - GCDAsyncSocketSSLProtocolVersionMin
+ * - GCDAsyncSocketSSLProtocolVersionMax
+ *
+ * If SecureTransport is available on Mac OS X:
+ *
+ * - GCDAsyncSocketSSLCipherSuites
+ * - GCDAsyncSocketSSLDiffieHellmanParameters;
+ *
+ *
+ * Please refer to Apple's documentation for associated values, as well as other possible keys.
+ *
+ * If you pass in nil or an empty dictionary, the default settings will be used.
+ *
+ * The default settings will check to make sure the remote party's certificate is signed by a
+ * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired.
+ * However it will not verify the name on the certificate unless you
+ * give it a name to verify against via the kCFStreamSSLPeerName key.
+ * The security implications of this are important to understand.
+ * Imagine you are attempting to create a secure connection to MySecureServer.com,
+ * but your socket gets directed to MaliciousServer.com because of a hacked DNS server.
+ * If you simply use the default settings, and MaliciousServer.com has a valid certificate,
+ * the default settings will not detect any problems since the certificate is valid.
+ * To properly secure your connection in this particular scenario you
+ * should set the kCFStreamSSLPeerName property to "MySecureServer.com".
+ * If you do not know the peer name of the remote host in advance (for example, you're not sure
+ * if it will be "domain.com" or "www.domain.com"), then you can use the default settings to validate the
+ * certificate, and then use the X509Certificate class to verify the issuer after the socket has been secured.
+ * The X509Certificate class is part of the CocoaAsyncSocket open source project.
+ **/
+- (void)startTLS:(NSDictionary *)tlsSettings;
+
+#pragma mark Advanced
+
+/**
+ * Traditionally sockets are not closed until the conversation is over.
+ * However, it is technically possible for the remote enpoint to close its write stream.
+ * Our socket would then be notified that there is no more data to be read,
+ * but our socket would still be writeable and the remote endpoint could continue to receive our data.
+ *
+ * The argument for this confusing functionality stems from the idea that a client could shut down its
+ * write stream after sending a request to the server, thus notifying the server there are to be no further requests.
+ * In practice, however, this technique did little to help server developers.
+ *
+ * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close
+ * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell
+ * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work.
+ * Otherwise an error will be occur shortly (when the remote end sends us a RST packet).
+ *
+ * In addition to the technical challenges and confusion, many high level socket/stream API's provide
+ * no support for dealing with the problem. If the read stream is closed, the API immediately declares the
+ * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does.
+ * It might sound like poor design at first, but in fact it simplifies development.
+ *
+ * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket.
+ * Thus it actually makes sense to close the socket at this point.
+ * And in fact this is what most networking developers want and expect to happen.
+ * However, if you are writing a server that interacts with a plethora of clients,
+ * you might encounter a client that uses the discouraged technique of shutting down its write stream.
+ * If this is the case, you can set this property to NO,
+ * and make use of the socketDidCloseReadStream delegate method.
+ *
+ * The default value is YES.
+**/
+- (BOOL)autoDisconnectOnClosedReadStream;
+- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag;
+
+/**
+ * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue.
+ * In most cases, the instance creates this queue itself.
+ * However, to allow for maximum flexibility, the internal queue may be passed in the init method.
+ * This allows for some advanced options such as controlling socket priority via target queues.
+ * However, when one begins to use target queues like this, they open the door to some specific deadlock issues.
+ *
+ * For example, imagine there are 2 queues:
+ * dispatch_queue_t socketQueue;
+ * dispatch_queue_t socketTargetQueue;
+ *
+ * If you do this (pseudo-code):
+ * socketQueue.targetQueue = socketTargetQueue;
+ *
+ * Then all socketQueue operations will actually get run on the given socketTargetQueue.
+ * This is fine and works great in most situations.
+ * But if you run code directly from within the socketTargetQueue that accesses the socket,
+ * you could potentially get deadlock. Imagine the following code:
+ *
+ * - (BOOL)socketHasSomething
+ * {
+ * __block BOOL result = NO;
+ * dispatch_block_t block = ^{
+ * result = [self someInternalMethodToBeRunOnlyOnSocketQueue];
+ * }
+ * if (is_executing_on_queue(socketQueue))
+ * block();
+ * else
+ * dispatch_sync(socketQueue, block);
+ *
+ * return result;
+ * }
+ *
+ * What happens if you call this method from the socketTargetQueue? The result is deadlock.
+ * This is because the GCD API offers no mechanism to discover a queue's targetQueue.
+ * Thus we have no idea if our socketQueue is configured with a targetQueue.
+ * If we had this information, we could easily avoid deadlock.
+ * But, since these API's are missing or unfeasible, you'll have to explicitly set it.
+ *
+ * IF you pass a socketQueue via the init method,
+ * AND you've configured the passed socketQueue with a targetQueue,
+ * THEN you should pass the end queue in the target hierarchy.
+ *
+ * For example, consider the following queue hierarchy:
+ * socketQueue -> ipQueue -> moduleQueue
+ *
+ * This example demonstrates priority shaping within some server.
+ * All incoming client connections from the same IP address are executed on the same target queue.
+ * And all connections for a particular module are executed on the same target queue.
+ * Thus, the priority of all networking for the entire module can be changed on the fly.
+ * Additionally, networking traffic from a single IP cannot monopolize the module.
+ *
+ * Here's how you would accomplish something like that:
+ * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock
+ * {
+ * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL);
+ * dispatch_queue_t ipQueue = [self ipQueueForAddress:address];
+ *
+ * dispatch_set_target_queue(socketQueue, ipQueue);
+ * dispatch_set_target_queue(iqQueue, moduleQueue);
+ *
+ * return socketQueue;
+ * }
+ * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
+ * {
+ * [clientConnections addObject:newSocket];
+ * [newSocket markSocketQueueTargetQueue:moduleQueue];
+ * }
+ *
+ * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue.
+ * This is often NOT the case, as such queues are used solely for execution shaping.
+**/
+- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue;
+- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue;
+
+/**
+ * It's not thread-safe to access certain variables from outside the socket's internal queue.
+ *
+ * For example, the socket file descriptor.
+ * File descriptors are simply integers which reference an index in the per-process file table.
+ * However, when one requests a new file descriptor (by opening a file or socket),
+ * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor.
+ * So if we're not careful, the following could be possible:
+ *
+ * - Thread A invokes a method which returns the socket's file descriptor.
+ * - The socket is closed via the socket's internal queue on thread B.
+ * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD.
+ * - Thread A is now accessing/altering the file instead of the socket.
+ *
+ * In addition to this, other variables are not actually objects,
+ * and thus cannot be retained/released or even autoreleased.
+ * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct.
+ *
+ * Although there are internal variables that make it difficult to maintain thread-safety,
+ * it is important to provide access to these variables
+ * to ensure this class can be used in a wide array of environments.
+ * This method helps to accomplish this by invoking the current block on the socket's internal queue.
+ * The methods below can be invoked from within the block to access
+ * those generally thread-unsafe internal variables in a thread-safe manner.
+ * The given block will be invoked synchronously on the socket's internal queue.
+ *
+ * If you save references to any protected variables and use them outside the block, you do so at your own peril.
+**/
+- (void)performBlock:(dispatch_block_t)block;
+
+/**
+ * These methods are only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Provides access to the socket's file descriptor(s).
+ * If the socket is a server socket (is accepting incoming connections),
+ * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6.
+**/
+- (int)socketFD;
+- (int)socket4FD;
+- (int)socket6FD;
+
+#if TARGET_OS_IPHONE
+
+/**
+ * These methods are only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Provides access to the socket's internal CFReadStream/CFWriteStream.
+ *
+ * These streams are only used as workarounds for specific iOS shortcomings:
+ *
+ * - Apple has decided to keep the SecureTransport framework private is iOS.
+ * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it.
+ * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream,
+ * instead of the preferred and faster and more powerful SecureTransport.
+ *
+ * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded,
+ * Apple only bothers to notify us via the CFStream API.
+ * The faster and more powerful GCD API isn't notified properly in this case.
+ *
+ * See also: (BOOL)enableBackgroundingOnSocket
+**/
+- (CFReadStreamRef)readStream;
+- (CFWriteStreamRef)writeStream;
+
+/**
+ * This method is only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Configures the socket to allow it to operate when the iOS application has been backgrounded.
+ * In other words, this method creates a read & write stream, and invokes:
+ *
+ * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+ * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+ *
+ * Returns YES if successful, NO otherwise.
+ *
+ * Note: Apple does not officially support backgrounding server sockets.
+ * That is, if your socket is accepting incoming connections, Apple does not officially support
+ * allowing iOS applications to accept incoming connections while an app is backgrounded.
+ *
+ * Example usage:
+ *
+ * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
+ * {
+ * [asyncSocket performBlock:^{
+ * [asyncSocket enableBackgroundingOnSocket];
+ * }];
+ * }
+**/
+- (BOOL)enableBackgroundingOnSocket;
+
+#endif
+
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+/**
+ * This method is only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket.
+**/
+- (SSLContextRef)sslContext;
+
+#endif
+
+#pragma mark Utilities
+
+/**
+ * Extracting host and port information from raw address data.
+**/
++ (NSString *)hostFromAddress:(NSData *)address;
++ (uint16_t)portFromAddress:(NSData *)address;
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address;
+
+/**
+ * A few common line separators, for use with the readDataToData:... methods.
+**/
++ (NSData *)CRLFData; // 0x0D0A
++ (NSData *)CRData; // 0x0D
++ (NSData *)LFData; // 0x0A
++ (NSData *)ZeroData; // 0x00
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@protocol GCDAsyncSocketDelegate
+@optional
+
+/**
+ * This method is called immediately prior to socket:didAcceptNewSocket:.
+ * It optionally allows a listening socket to specify the socketQueue for a new accepted socket.
+ * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue.
+ *
+ * Since you cannot autorelease a dispatch_queue,
+ * this method uses the "new" prefix in its name to specify that the returned queue has been retained.
+ *
+ * Thus you could do something like this in the implementation:
+ * return dispatch_queue_create("MyQueue", NULL);
+ *
+ * If you are placing multiple sockets on the same queue,
+ * then care should be taken to increment the retain count each time this method is invoked.
+ *
+ * For example, your implementation might look something like this:
+ * dispatch_retain(myExistingQueue);
+ * return myExistingQueue;
+**/
+- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock;
+
+/**
+ * Called when a socket accepts a connection.
+ * Another socket is automatically spawned to handle it.
+ *
+ * You must retain the newSocket if you wish to handle the connection.
+ * Otherwise the newSocket instance will be released and the spawned connection will be closed.
+ *
+ * By default the new socket will have the same delegate and delegateQueue.
+ * You may, of course, change this at any time.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket;
+
+/**
+ * Called when a socket connects and is ready for reading and writing.
+ * The host parameter will be an IP address, not a DNS name.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;
+
+/**
+ * Called when a socket has completed reading the requested data into memory.
+ * Not called if there is an error.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag;
+
+/**
+ * Called when a socket has read in data, but has not yet completed the read.
+ * This would occur if using readToData: or readToLength: methods.
+ * It may be used to for things such as updating progress bars.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag;
+
+/**
+ * Called when a socket has completed writing the requested data. Not called if there is an error.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag;
+
+/**
+ * Called when a socket has written some data, but has not yet completed the entire write.
+ * It may be used to for things such as updating progress bars.
+**/
+- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag;
+
+/**
+ * Called if a read operation has reached its timeout without completing.
+ * This method allows you to optionally extend the timeout.
+ * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount.
+ * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual.
+ *
+ * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
+ * The length parameter is the number of bytes that have been read so far for the read operation.
+ *
+ * Note that this method may be called multiple times for a single read if you return positive numbers.
+**/
+- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag
+ elapsed:(NSTimeInterval)elapsed
+ bytesDone:(NSUInteger)length;
+
+/**
+ * Called if a write operation has reached its timeout without completing.
+ * This method allows you to optionally extend the timeout.
+ * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount.
+ * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual.
+ *
+ * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
+ * The length parameter is the number of bytes that have been written so far for the write operation.
+ *
+ * Note that this method may be called multiple times for a single write if you return positive numbers.
+**/
+- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag
+ elapsed:(NSTimeInterval)elapsed
+ bytesDone:(NSUInteger)length;
+
+/**
+ * Conditionally called if the read stream closes, but the write stream may still be writeable.
+ *
+ * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO.
+ * See the discussion on the autoDisconnectOnClosedReadStream method for more information.
+**/
+- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock;
+
+/**
+ * Called when a socket disconnects with or without error.
+ *
+ * If you call the disconnect method, and the socket wasn't already disconnected,
+ * this delegate method will be called before the disconnect method returns.
+**/
+- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err;
+
+/**
+ * Called after the socket has successfully completed SSL/TLS negotiation.
+ * This method is not called unless you use the provided startTLS method.
+ *
+ * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close,
+ * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code.
+**/
+- (void)socketDidSecure:(GCDAsyncSocket *)sock;
+
+@end
--- /dev/null
+//
+// GCDAsyncSocket.m
+//
+// This class is in the public domain.
+// Originally created by Robbie Hanson in Q4 2010.
+// Updated and maintained by Deusty LLC and the Apple development community.
+//
+// https://github.com/robbiehanson/CocoaAsyncSocket
+//
+
+#import "GCDAsyncSocket.h"
+
+#if TARGET_OS_IPHONE
+#import <CFNetwork/CFNetwork.h>
+#endif
+
+#import <arpa/inet.h>
+#import <fcntl.h>
+#import <ifaddrs.h>
+#import <netdb.h>
+#import <netinet/in.h>
+#import <net/if.h>
+#import <sys/socket.h>
+#import <sys/types.h>
+#import <sys/ioctl.h>
+#import <sys/poll.h>
+#import <sys/uio.h>
+#import <unistd.h>
+
+#if ! __has_feature(objc_arc)
+#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
+// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC
+#endif
+
+/**
+ * Does ARC support support GCD objects?
+ * It does if the minimum deployment target is iOS 6+ or Mac OS X 10.8+
+**/
+#if TARGET_OS_IPHONE
+
+ // Compiling for iOS
+
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 0
+ #else // iOS 5.X or earlier
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 1
+ #endif
+
+#else
+
+ // Compiling for Mac OS X
+
+ #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 0
+ #else
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier
+ #endif
+
+#endif
+
+
+#if 0
+
+// Logging Enabled - See log level below
+
+// Logging uses the CocoaLumberjack framework (which is also GCD based).
+// https://github.com/robbiehanson/CocoaLumberjack
+//
+// It allows us to do a lot of logging without significantly slowing down the code.
+#import "DDLog.h"
+
+#define LogAsync YES
+#define LogContext 65535
+
+#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
+#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
+
+#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+
+#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+
+#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
+#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
+
+// Log levels : off, error, warn, info, verbose
+static const int logLevel = LOG_LEVEL_VERBOSE;
+
+#else
+
+// Logging Disabled
+
+#define LogError(frmt, ...) {}
+#define LogWarn(frmt, ...) {}
+#define LogInfo(frmt, ...) {}
+#define LogVerbose(frmt, ...) {}
+
+#define LogCError(frmt, ...) {}
+#define LogCWarn(frmt, ...) {}
+#define LogCInfo(frmt, ...) {}
+#define LogCVerbose(frmt, ...) {}
+
+#define LogTrace() {}
+#define LogCTrace(frmt, ...) {}
+
+#endif
+
+/**
+ * Seeing a return statements within an inner block
+ * can sometimes be mistaken for a return point of the enclosing method.
+ * This makes inline blocks a bit easier to read.
+**/
+#define return_from_block return
+
+/**
+ * A socket file descriptor is really just an integer.
+ * It represents the index of the socket within the kernel.
+ * This makes invalid file descriptor comparisons easier to read.
+**/
+#define SOCKET_NULL -1
+
+
+NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException";
+NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain";
+
+NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket";
+NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream";
+
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites";
+#if TARGET_OS_IPHONE
+NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin";
+NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax";
+#else
+NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters";
+#endif
+#endif
+
+enum GCDAsyncSocketFlags
+{
+ kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting)
+ kConnected = 1 << 1, // If set, the socket is connected
+ kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed
+ kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout
+ kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout
+ kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued
+ kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued
+ kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown.
+ kReadSourceSuspended = 1 << 8, // If set, the read source is suspended
+ kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended
+ kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS
+ kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete
+ kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete
+ kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS
+ kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket
+ kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained
+#if TARGET_OS_IPHONE
+ kAddedStreamsToRunLoop = 1 << 16, // If set, CFStreams have been added to listener thread
+ kUsingCFStreamForTLS = 1 << 17, // If set, we're forced to use CFStream instead of SecureTransport
+ kSecureSocketHasBytesAvailable = 1 << 18, // If set, CFReadStream has notified us of bytes available
+#endif
+};
+
+enum GCDAsyncSocketConfig
+{
+ kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled
+ kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled
+ kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4
+ kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes
+};
+
+#if TARGET_OS_IPHONE
+ static NSThread *cfstreamThread; // Used for CFStreams
+#endif
+
+@interface GCDAsyncSocket ()
+{
+ uint32_t flags;
+ uint16_t config;
+
+#if __has_feature(objc_arc_weak)
+ __weak id delegate;
+#else
+ __unsafe_unretained id delegate;
+#endif
+ dispatch_queue_t delegateQueue;
+
+ int socket4FD;
+ int socket6FD;
+ int connectIndex;
+ NSData * connectInterface4;
+ NSData * connectInterface6;
+
+ dispatch_queue_t socketQueue;
+
+ dispatch_source_t accept4Source;
+ dispatch_source_t accept6Source;
+ dispatch_source_t connectTimer;
+ dispatch_source_t readSource;
+ dispatch_source_t writeSource;
+ dispatch_source_t readTimer;
+ dispatch_source_t writeTimer;
+
+ NSMutableArray *readQueue;
+ NSMutableArray *writeQueue;
+
+ GCDAsyncReadPacket *currentRead;
+ GCDAsyncWritePacket *currentWrite;
+
+ unsigned long socketFDBytesAvailable;
+
+ GCDAsyncSocketPreBuffer *preBuffer;
+
+#if TARGET_OS_IPHONE
+ CFStreamClientContext streamContext;
+ CFReadStreamRef readStream;
+ CFWriteStreamRef writeStream;
+#endif
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+ SSLContextRef sslContext;
+ GCDAsyncSocketPreBuffer *sslPreBuffer;
+ size_t sslWriteCachedLength;
+ OSStatus sslErrCode;
+#endif
+
+ void *IsOnSocketQueueOrTargetQueueKey;
+
+ id userData;
+}
+// Accepting
+- (BOOL)doAccept:(int)socketFD;
+
+// Connecting
+- (void)startConnectTimeout:(NSTimeInterval)timeout;
+- (void)endConnectTimeout;
+- (void)doConnectTimeout;
+- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port;
+- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6;
+- (void)lookup:(int)aConnectIndex didFail:(NSError *)error;
+- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr;
+- (void)didConnect:(int)aConnectIndex;
+- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error;
+
+// Disconnect
+- (void)closeWithError:(NSError *)error;
+- (void)maybeClose;
+
+// Errors
+- (NSError *)badConfigError:(NSString *)msg;
+- (NSError *)badParamError:(NSString *)msg;
+- (NSError *)gaiError:(int)gai_error;
+- (NSError *)errnoError;
+- (NSError *)errnoErrorWithReason:(NSString *)reason;
+- (NSError *)connectTimeoutError;
+- (NSError *)otherError:(NSString *)msg;
+
+// Diagnostics
+- (NSString *)connectedHost4;
+- (NSString *)connectedHost6;
+- (uint16_t)connectedPort4;
+- (uint16_t)connectedPort6;
+- (NSString *)localHost4;
+- (NSString *)localHost6;
+- (uint16_t)localPort4;
+- (uint16_t)localPort6;
+- (NSString *)connectedHostFromSocket4:(int)socketFD;
+- (NSString *)connectedHostFromSocket6:(int)socketFD;
+- (uint16_t)connectedPortFromSocket4:(int)socketFD;
+- (uint16_t)connectedPortFromSocket6:(int)socketFD;
+- (NSString *)localHostFromSocket4:(int)socketFD;
+- (NSString *)localHostFromSocket6:(int)socketFD;
+- (uint16_t)localPortFromSocket4:(int)socketFD;
+- (uint16_t)localPortFromSocket6:(int)socketFD;
+
+// Utilities
+- (void)getInterfaceAddress4:(NSMutableData **)addr4Ptr
+ address6:(NSMutableData **)addr6Ptr
+ fromDescription:(NSString *)interfaceDescription
+ port:(uint16_t)port;
+- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD;
+- (void)suspendReadSource;
+- (void)resumeReadSource;
+- (void)suspendWriteSource;
+- (void)resumeWriteSource;
+
+// Reading
+- (void)maybeDequeueRead;
+- (void)flushSSLBuffers;
+- (void)doReadData;
+- (void)doReadEOF;
+- (void)completeCurrentRead;
+- (void)endCurrentRead;
+- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout;
+- (void)doReadTimeout;
+- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension;
+
+// Writing
+- (void)maybeDequeueWrite;
+- (void)doWriteData;
+- (void)completeCurrentWrite;
+- (void)endCurrentWrite;
+- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout;
+- (void)doWriteTimeout;
+- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension;
+
+// Security
+- (void)maybeStartTLS;
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+- (void)ssl_startTLS;
+- (void)ssl_continueSSLHandshake;
+#endif
+#if TARGET_OS_IPHONE
+- (void)cf_startTLS;
+#endif
+
+// CFStream
+#if TARGET_OS_IPHONE
++ (void)startCFStreamThreadIfNeeded;
+- (BOOL)createReadAndWriteStream;
+- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite;
+- (BOOL)addStreamsToRunLoop;
+- (BOOL)openStreams;
+- (void)removeStreamsFromRunLoop;
+#endif
+
+// Class Methods
++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A PreBuffer is used when there is more data available on the socket
+ * than is being requested by current read request.
+ * In this case we slurp up all data from the socket (to minimize sys calls),
+ * and store additional yet unread data in a "prebuffer".
+ *
+ * The prebuffer is entirely drained before we read from the socket again.
+ * In other words, a large chunk of data is written is written to the prebuffer.
+ * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)).
+ *
+ * A ring buffer was once used for this purpose.
+ * But a ring buffer takes up twice as much memory as needed (double the size for mirroring).
+ * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size.
+ * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed.
+ *
+ * The current design is very simple and straight-forward, while also keeping memory requirements lower.
+**/
+
+@interface GCDAsyncSocketPreBuffer : NSObject
+{
+ uint8_t *preBuffer;
+ size_t preBufferSize;
+
+ uint8_t *readPointer;
+ uint8_t *writePointer;
+}
+
+- (id)initWithCapacity:(size_t)numBytes;
+
+- (void)ensureCapacityForWrite:(size_t)numBytes;
+
+- (size_t)availableBytes;
+- (uint8_t *)readBuffer;
+
+- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr;
+
+- (size_t)availableSpace;
+- (uint8_t *)writeBuffer;
+
+- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr;
+
+- (void)didRead:(size_t)bytesRead;
+- (void)didWrite:(size_t)bytesWritten;
+
+- (void)reset;
+
+@end
+
+@implementation GCDAsyncSocketPreBuffer
+
+- (id)initWithCapacity:(size_t)numBytes
+{
+ if ((self = [super init]))
+ {
+ preBufferSize = numBytes;
+ preBuffer = malloc(preBufferSize);
+
+ readPointer = preBuffer;
+ writePointer = preBuffer;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ if (preBuffer)
+ free(preBuffer);
+}
+
+- (void)ensureCapacityForWrite:(size_t)numBytes
+{
+ size_t availableSpace = preBufferSize - (writePointer - readPointer);
+
+ if (numBytes > availableSpace)
+ {
+ size_t additionalBytes = numBytes - availableSpace;
+
+ size_t newPreBufferSize = preBufferSize + additionalBytes;
+ uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize);
+
+ size_t readPointerOffset = readPointer - preBuffer;
+ size_t writePointerOffset = writePointer - preBuffer;
+
+ preBuffer = newPreBuffer;
+ preBufferSize = newPreBufferSize;
+
+ readPointer = preBuffer + readPointerOffset;
+ writePointer = preBuffer + writePointerOffset;
+ }
+}
+
+- (size_t)availableBytes
+{
+ return writePointer - readPointer;
+}
+
+- (uint8_t *)readBuffer
+{
+ return readPointer;
+}
+
+- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr
+{
+ if (bufferPtr) *bufferPtr = readPointer;
+ if (availableBytesPtr) *availableBytesPtr = writePointer - readPointer;
+}
+
+- (void)didRead:(size_t)bytesRead
+{
+ readPointer += bytesRead;
+
+ if (readPointer == writePointer)
+ {
+ // The prebuffer has been drained. Reset pointers.
+ readPointer = preBuffer;
+ writePointer = preBuffer;
+ }
+}
+
+- (size_t)availableSpace
+{
+ return preBufferSize - (writePointer - readPointer);
+}
+
+- (uint8_t *)writeBuffer
+{
+ return writePointer;
+}
+
+- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr
+{
+ if (bufferPtr) *bufferPtr = writePointer;
+ if (availableSpacePtr) *availableSpacePtr = preBufferSize - (writePointer - readPointer);
+}
+
+- (void)didWrite:(size_t)bytesWritten
+{
+ writePointer += bytesWritten;
+}
+
+- (void)reset
+{
+ readPointer = preBuffer;
+ writePointer = preBuffer;
+}
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * The GCDAsyncReadPacket encompasses the instructions for any given read.
+ * The content of a read packet allows the code to determine if we're:
+ * - reading to a certain length
+ * - reading to a certain separator
+ * - or simply reading the first chunk of available data
+**/
+@interface GCDAsyncReadPacket : NSObject
+{
+ @public
+ NSMutableData *buffer;
+ NSUInteger startOffset;
+ NSUInteger bytesDone;
+ NSUInteger maxLength;
+ NSTimeInterval timeout;
+ NSUInteger readLength;
+ NSData *term;
+ BOOL bufferOwner;
+ NSUInteger originalBufferLength;
+ long tag;
+}
+- (id)initWithData:(NSMutableData *)d
+ startOffset:(NSUInteger)s
+ maxLength:(NSUInteger)m
+ timeout:(NSTimeInterval)t
+ readLength:(NSUInteger)l
+ terminator:(NSData *)e
+ tag:(long)i;
+
+- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
+
+- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
+
+- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;
+- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
+- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
+
+- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;
+
+@end
+
+@implementation GCDAsyncReadPacket
+
+- (id)initWithData:(NSMutableData *)d
+ startOffset:(NSUInteger)s
+ maxLength:(NSUInteger)m
+ timeout:(NSTimeInterval)t
+ readLength:(NSUInteger)l
+ terminator:(NSData *)e
+ tag:(long)i
+{
+ if((self = [super init]))
+ {
+ bytesDone = 0;
+ maxLength = m;
+ timeout = t;
+ readLength = l;
+ term = [e copy];
+ tag = i;
+
+ if (d)
+ {
+ buffer = d;
+ startOffset = s;
+ bufferOwner = NO;
+ originalBufferLength = [d length];
+ }
+ else
+ {
+ if (readLength > 0)
+ buffer = [[NSMutableData alloc] initWithLength:readLength];
+ else
+ buffer = [[NSMutableData alloc] initWithLength:0];
+
+ startOffset = 0;
+ bufferOwner = YES;
+ originalBufferLength = 0;
+ }
+ }
+ return self;
+}
+
+/**
+ * Increases the length of the buffer (if needed) to ensure a read of the given size will fit.
+**/
+- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead
+{
+ NSUInteger buffSize = [buffer length];
+ NSUInteger buffUsed = startOffset + bytesDone;
+
+ NSUInteger buffSpace = buffSize - buffUsed;
+
+ if (bytesToRead > buffSpace)
+ {
+ NSUInteger buffInc = bytesToRead - buffSpace;
+
+ [buffer increaseLengthBy:buffInc];
+ }
+}
+
+/**
+ * This method is used when we do NOT know how much data is available to be read from the socket.
+ * This method returns the default value unless it exceeds the specified readLength or maxLength.
+ *
+ * Furthermore, the shouldPreBuffer decision is based upon the packet type,
+ * and whether the returned value would fit in the current buffer without requiring a resize of the buffer.
+**/
+- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr
+{
+ NSUInteger result;
+
+ if (readLength > 0)
+ {
+ // Read a specific length of data
+
+ result = MIN(defaultValue, (readLength - bytesDone));
+
+ // There is no need to prebuffer since we know exactly how much data we need to read.
+ // Even if the buffer isn't currently big enough to fit this amount of data,
+ // it would have to be resized eventually anyway.
+
+ if (shouldPreBufferPtr)
+ *shouldPreBufferPtr = NO;
+ }
+ else
+ {
+ // Either reading until we find a specified terminator,
+ // or we're simply reading all available data.
+ //
+ // In other words, one of:
+ //
+ // - readDataToData packet
+ // - readDataWithTimeout packet
+
+ if (maxLength > 0)
+ result = MIN(defaultValue, (maxLength - bytesDone));
+ else
+ result = defaultValue;
+
+ // Since we don't know the size of the read in advance,
+ // the shouldPreBuffer decision is based upon whether the returned value would fit
+ // in the current buffer without requiring a resize of the buffer.
+ //
+ // This is because, in all likelyhood, the amount read from the socket will be less than the default value.
+ // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead.
+
+ if (shouldPreBufferPtr)
+ {
+ NSUInteger buffSize = [buffer length];
+ NSUInteger buffUsed = startOffset + bytesDone;
+
+ NSUInteger buffSpace = buffSize - buffUsed;
+
+ if (buffSpace >= result)
+ *shouldPreBufferPtr = NO;
+ else
+ *shouldPreBufferPtr = YES;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * For read packets without a set terminator, returns the amount of data
+ * that can be read without exceeding the readLength or maxLength.
+ *
+ * The given parameter indicates the number of bytes estimated to be available on the socket,
+ * which is taken into consideration during the calculation.
+ *
+ * The given hint MUST be greater than zero.
+**/
+- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable
+{
+ NSAssert(term == nil, @"This method does not apply to term reads");
+ NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable");
+
+ if (readLength > 0)
+ {
+ // Read a specific length of data
+
+ return MIN(bytesAvailable, (readLength - bytesDone));
+
+ // No need to avoid resizing the buffer.
+ // If the user provided their own buffer,
+ // and told us to read a certain length of data that exceeds the size of the buffer,
+ // then it is clear that our code will resize the buffer during the read operation.
+ //
+ // This method does not actually do any resizing.
+ // The resizing will happen elsewhere if needed.
+ }
+ else
+ {
+ // Read all available data
+
+ NSUInteger result = bytesAvailable;
+
+ if (maxLength > 0)
+ {
+ result = MIN(result, (maxLength - bytesDone));
+ }
+
+ // No need to avoid resizing the buffer.
+ // If the user provided their own buffer,
+ // and told us to read all available data without giving us a maxLength,
+ // then it is clear that our code might resize the buffer during the read operation.
+ //
+ // This method does not actually do any resizing.
+ // The resizing will happen elsewhere if needed.
+
+ return result;
+ }
+}
+
+/**
+ * For read packets with a set terminator, returns the amount of data
+ * that can be read without exceeding the maxLength.
+ *
+ * The given parameter indicates the number of bytes estimated to be available on the socket,
+ * which is taken into consideration during the calculation.
+ *
+ * To optimize memory allocations, mem copies, and mem moves
+ * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first,
+ * or if the data can be read directly into the read packet's buffer.
+**/
+- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr
+{
+ NSAssert(term != nil, @"This method does not apply to non-term reads");
+ NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable");
+
+
+ NSUInteger result = bytesAvailable;
+
+ if (maxLength > 0)
+ {
+ result = MIN(result, (maxLength - bytesDone));
+ }
+
+ // Should the data be read into the read packet's buffer, or into a pre-buffer first?
+ //
+ // One would imagine the preferred option is the faster one.
+ // So which one is faster?
+ //
+ // Reading directly into the packet's buffer requires:
+ // 1. Possibly resizing packet buffer (malloc/realloc)
+ // 2. Filling buffer (read)
+ // 3. Searching for term (memcmp)
+ // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy)
+ //
+ // Reading into prebuffer first:
+ // 1. Possibly resizing prebuffer (malloc/realloc)
+ // 2. Filling buffer (read)
+ // 3. Searching for term (memcmp)
+ // 4. Copying underflow into packet buffer (malloc/realloc, memcpy)
+ // 5. Removing underflow from prebuffer (memmove)
+ //
+ // Comparing the performance of the two we can see that reading
+ // data into the prebuffer first is slower due to the extra memove.
+ //
+ // However:
+ // The implementation of NSMutableData is open source via core foundation's CFMutableData.
+ // Decreasing the length of a mutable data object doesn't cause a realloc.
+ // In other words, the capacity of a mutable data object can grow, but doesn't shrink.
+ //
+ // This means the prebuffer will rarely need a realloc.
+ // The packet buffer, on the other hand, may often need a realloc.
+ // This is especially true if we are the buffer owner.
+ // Furthermore, if we are constantly realloc'ing the packet buffer,
+ // and then moving the overflow into the prebuffer,
+ // then we're consistently over-allocating memory for each term read.
+ // And now we get into a bit of a tradeoff between speed and memory utilization.
+ //
+ // The end result is that the two perform very similarly.
+ // And we can answer the original question very simply by another means.
+ //
+ // If we can read all the data directly into the packet's buffer without resizing it first,
+ // then we do so. Otherwise we use the prebuffer.
+
+ if (shouldPreBufferPtr)
+ {
+ NSUInteger buffSize = [buffer length];
+ NSUInteger buffUsed = startOffset + bytesDone;
+
+ if ((buffSize - buffUsed) >= result)
+ *shouldPreBufferPtr = NO;
+ else
+ *shouldPreBufferPtr = YES;
+ }
+
+ return result;
+}
+
+/**
+ * For read packets with a set terminator,
+ * returns the amount of data that can be read from the given preBuffer,
+ * without going over a terminator or the maxLength.
+ *
+ * It is assumed the terminator has not already been read.
+**/
+- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr
+{
+ NSAssert(term != nil, @"This method does not apply to non-term reads");
+ NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!");
+
+ // We know that the terminator, as a whole, doesn't exist in our own buffer.
+ // But it is possible that a _portion_ of it exists in our buffer.
+ // So we're going to look for the terminator starting with a portion of our own buffer.
+ //
+ // Example:
+ //
+ // term length = 3 bytes
+ // bytesDone = 5 bytes
+ // preBuffer length = 5 bytes
+ //
+ // If we append the preBuffer to our buffer,
+ // it would look like this:
+ //
+ // ---------------------
+ // |B|B|B|B|B|P|P|P|P|P|
+ // ---------------------
+ //
+ // So we start our search here:
+ //
+ // ---------------------
+ // |B|B|B|B|B|P|P|P|P|P|
+ // -------^-^-^---------
+ //
+ // And move forwards...
+ //
+ // ---------------------
+ // |B|B|B|B|B|P|P|P|P|P|
+ // ---------^-^-^-------
+ //
+ // Until we find the terminator or reach the end.
+ //
+ // ---------------------
+ // |B|B|B|B|B|P|P|P|P|P|
+ // ---------------^-^-^-
+
+ BOOL found = NO;
+
+ NSUInteger termLength = [term length];
+ NSUInteger preBufferLength = [preBuffer availableBytes];
+
+ if ((bytesDone + preBufferLength) < termLength)
+ {
+ // Not enough data for a full term sequence yet
+ return preBufferLength;
+ }
+
+ NSUInteger maxPreBufferLength;
+ if (maxLength > 0) {
+ maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone));
+
+ // Note: maxLength >= termLength
+ }
+ else {
+ maxPreBufferLength = preBufferLength;
+ }
+
+ uint8_t seq[termLength];
+ const void *termBuf = [term bytes];
+
+ NSUInteger bufLen = MIN(bytesDone, (termLength - 1));
+ uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen;
+
+ NSUInteger preLen = termLength - bufLen;
+ const uint8_t *pre = [preBuffer readBuffer];
+
+ NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above.
+
+ NSUInteger result = maxPreBufferLength;
+
+ NSUInteger i;
+ for (i = 0; i < loopCount; i++)
+ {
+ if (bufLen > 0)
+ {
+ // Combining bytes from buffer and preBuffer
+
+ memcpy(seq, buf, bufLen);
+ memcpy(seq + bufLen, pre, preLen);
+
+ if (memcmp(seq, termBuf, termLength) == 0)
+ {
+ result = preLen;
+ found = YES;
+ break;
+ }
+
+ buf++;
+ bufLen--;
+ preLen++;
+ }
+ else
+ {
+ // Comparing directly from preBuffer
+
+ if (memcmp(pre, termBuf, termLength) == 0)
+ {
+ NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic
+
+ result = preOffset + termLength;
+ found = YES;
+ break;
+ }
+
+ pre++;
+ }
+ }
+
+ // There is no need to avoid resizing the buffer in this particular situation.
+
+ if (foundPtr) *foundPtr = found;
+ return result;
+}
+
+/**
+ * For read packets with a set terminator, scans the packet buffer for the term.
+ * It is assumed the terminator had not been fully read prior to the new bytes.
+ *
+ * If the term is found, the number of excess bytes after the term are returned.
+ * If the term is not found, this method will return -1.
+ *
+ * Note: A return value of zero means the term was found at the very end.
+ *
+ * Prerequisites:
+ * The given number of bytes have been added to the end of our buffer.
+ * Our bytesDone variable has NOT been changed due to the prebuffered bytes.
+**/
+- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes
+{
+ NSAssert(term != nil, @"This method does not apply to non-term reads");
+
+ // The implementation of this method is very similar to the above method.
+ // See the above method for a discussion of the algorithm used here.
+
+ uint8_t *buff = [buffer mutableBytes];
+ NSUInteger buffLength = bytesDone + numBytes;
+
+ const void *termBuff = [term bytes];
+ NSUInteger termLength = [term length];
+
+ // Note: We are dealing with unsigned integers,
+ // so make sure the math doesn't go below zero.
+
+ NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0;
+
+ while (i + termLength <= buffLength)
+ {
+ uint8_t *subBuffer = buff + startOffset + i;
+
+ if (memcmp(subBuffer, termBuff, termLength) == 0)
+ {
+ return buffLength - (i + termLength);
+ }
+
+ i++;
+ }
+
+ return -1;
+}
+
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * The GCDAsyncWritePacket encompasses the instructions for any given write.
+**/
+@interface GCDAsyncWritePacket : NSObject
+{
+ @public
+ NSData *buffer;
+ NSUInteger bytesDone;
+ long tag;
+ NSTimeInterval timeout;
+}
+- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i;
+@end
+
+@implementation GCDAsyncWritePacket
+
+- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i
+{
+ if((self = [super init]))
+ {
+ buffer = d; // Retain not copy. For performance as documented in header file.
+ bytesDone = 0;
+ timeout = t;
+ tag = i;
+ }
+ return self;
+}
+
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues.
+ * This class my be altered to support more than just TLS in the future.
+**/
+@interface GCDAsyncSpecialPacket : NSObject
+{
+ @public
+ NSDictionary *tlsSettings;
+}
+- (id)initWithTLSSettings:(NSDictionary *)settings;
+@end
+
+@implementation GCDAsyncSpecialPacket
+
+- (id)initWithTLSSettings:(NSDictionary *)settings
+{
+ if((self = [super init]))
+ {
+ tlsSettings = [settings copy];
+ }
+ return self;
+}
+
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation GCDAsyncSocket
+
+- (id)init
+{
+ return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL];
+}
+
+- (id)initWithSocketQueue:(dispatch_queue_t)sq
+{
+ return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq];
+}
+
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
+{
+ return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL];
+}
+
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
+{
+ if((self = [super init]))
+ {
+ delegate = aDelegate;
+ delegateQueue = dq;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (dq) dispatch_retain(dq);
+ #endif
+
+ socket4FD = SOCKET_NULL;
+ socket6FD = SOCKET_NULL;
+ connectIndex = 0;
+
+ if (sq)
+ {
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+
+ socketQueue = sq;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_retain(sq);
+ #endif
+ }
+ else
+ {
+ socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL);
+ }
+
+ // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
+ // From the documentation:
+ //
+ // > Keys are only compared as pointers and are never dereferenced.
+ // > Thus, you can use a pointer to a static variable for a specific subsystem or
+ // > any other value that allows you to identify the value uniquely.
+ //
+ // We're just going to use the memory address of an ivar.
+ // Specifically an ivar that is explicitly named for our purpose to make the code more readable.
+ //
+ // However, it feels tedious (and less readable) to include the "&" all the time:
+ // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
+ //
+ // So we're going to make it so it doesn't matter if we use the '&' or not,
+ // by assigning the value of the ivar to the address of the ivar.
+ // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
+
+ IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
+
+ void *nonNullUnusedPointer = (__bridge void *)self;
+ dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
+
+ readQueue = [[NSMutableArray alloc] initWithCapacity:5];
+ currentRead = nil;
+
+ writeQueue = [[NSMutableArray alloc] initWithCapacity:5];
+ currentWrite = nil;
+
+ preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ LogInfo(@"%@ - %@ (start)", THIS_METHOD, self);
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ [self closeWithError:nil];
+ }
+ else
+ {
+ dispatch_sync(socketQueue, ^{
+ [self closeWithError:nil];
+ });
+ }
+
+ delegate = nil;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ #endif
+ delegateQueue = NULL;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (socketQueue) dispatch_release(socketQueue);
+ #endif
+ socketQueue = NULL;
+
+ LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Configuration
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (id)delegate
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return delegate;
+ }
+ else
+ {
+ __block id result;
+
+ dispatch_sync(socketQueue, ^{
+ result = delegate;
+ });
+
+ return result;
+ }
+}
+
+- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+ delegate = newDelegate;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegate:(id)newDelegate
+{
+ [self setDelegate:newDelegate synchronously:NO];
+}
+
+- (void)synchronouslySetDelegate:(id)newDelegate
+{
+ [self setDelegate:newDelegate synchronously:YES];
+}
+
+- (dispatch_queue_t)delegateQueue
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return delegateQueue;
+ }
+ else
+ {
+ __block dispatch_queue_t result;
+
+ dispatch_sync(socketQueue, ^{
+ result = delegateQueue;
+ });
+
+ return result;
+ }
+}
+
+- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ if (newDelegateQueue) dispatch_retain(newDelegateQueue);
+ #endif
+
+ delegateQueue = newDelegateQueue;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegateQueue:newDelegateQueue synchronously:NO];
+}
+
+- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegateQueue:newDelegateQueue synchronously:YES];
+}
+
+- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (delegatePtr) *delegatePtr = delegate;
+ if (delegateQueuePtr) *delegateQueuePtr = delegateQueue;
+ }
+ else
+ {
+ __block id dPtr = NULL;
+ __block dispatch_queue_t dqPtr = NULL;
+
+ dispatch_sync(socketQueue, ^{
+ dPtr = delegate;
+ dqPtr = delegateQueue;
+ });
+
+ if (delegatePtr) *delegatePtr = dPtr;
+ if (delegateQueuePtr) *delegateQueuePtr = dqPtr;
+ }
+}
+
+- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+
+ delegate = newDelegate;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ if (newDelegateQueue) dispatch_retain(newDelegateQueue);
+ #endif
+
+ delegateQueue = newDelegateQueue;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO];
+}
+
+- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES];
+}
+
+- (BOOL)isIPv4Enabled
+{
+ // Note: YES means kIPv4Disabled is OFF
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return ((config & kIPv4Disabled) == 0);
+ }
+ else
+ {
+ __block BOOL result;
+
+ dispatch_sync(socketQueue, ^{
+ result = ((config & kIPv4Disabled) == 0);
+ });
+
+ return result;
+ }
+}
+
+- (void)setIPv4Enabled:(BOOL)flag
+{
+ // Note: YES means kIPv4Disabled is OFF
+
+ dispatch_block_t block = ^{
+
+ if (flag)
+ config &= ~kIPv4Disabled;
+ else
+ config |= kIPv4Disabled;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (BOOL)isIPv6Enabled
+{
+ // Note: YES means kIPv6Disabled is OFF
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return ((config & kIPv6Disabled) == 0);
+ }
+ else
+ {
+ __block BOOL result;
+
+ dispatch_sync(socketQueue, ^{
+ result = ((config & kIPv6Disabled) == 0);
+ });
+
+ return result;
+ }
+}
+
+- (void)setIPv6Enabled:(BOOL)flag
+{
+ // Note: YES means kIPv6Disabled is OFF
+
+ dispatch_block_t block = ^{
+
+ if (flag)
+ config &= ~kIPv6Disabled;
+ else
+ config |= kIPv6Disabled;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (BOOL)isIPv4PreferredOverIPv6
+{
+ // Note: YES means kPreferIPv6 is OFF
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return ((config & kPreferIPv6) == 0);
+ }
+ else
+ {
+ __block BOOL result;
+
+ dispatch_sync(socketQueue, ^{
+ result = ((config & kPreferIPv6) == 0);
+ });
+
+ return result;
+ }
+}
+
+- (void)setPreferIPv4OverIPv6:(BOOL)flag
+{
+ // Note: YES means kPreferIPv6 is OFF
+
+ dispatch_block_t block = ^{
+
+ if (flag)
+ config &= ~kPreferIPv6;
+ else
+ config |= kPreferIPv6;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (id)userData
+{
+ __block id result = nil;
+
+ dispatch_block_t block = ^{
+
+ result = userData;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setUserData:(id)arbitraryUserData
+{
+ dispatch_block_t block = ^{
+
+ if (userData != arbitraryUserData)
+ {
+ userData = arbitraryUserData;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Accepting
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr
+{
+ return [self acceptOnInterface:nil port:port error:errPtr];
+}
+
+- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr
+{
+ LogTrace();
+
+ // Just in-case interface parameter is immutable.
+ NSString *interface = [inInterface copy];
+
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ // CreateSocket Block
+ // This block will be invoked within the dispatch block below.
+
+ int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) {
+
+ int socketFD = socket(domain, SOCK_STREAM, 0);
+
+ if (socketFD == SOCKET_NULL)
+ {
+ NSString *reason = @"Error in socket() function";
+ err = [self errnoErrorWithReason:reason];
+
+ return SOCKET_NULL;
+ }
+
+ int status;
+
+ // Set socket options
+
+ status = fcntl(socketFD, F_SETFL, O_NONBLOCK);
+ if (status == -1)
+ {
+ NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)";
+ err = [self errnoErrorWithReason:reason];
+
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ int reuseOn = 1;
+ status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
+ if (status == -1)
+ {
+ NSString *reason = @"Error enabling address reuse (setsockopt)";
+ err = [self errnoErrorWithReason:reason];
+
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ // Bind socket
+
+ status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]);
+ if (status == -1)
+ {
+ NSString *reason = @"Error in bind() function";
+ err = [self errnoErrorWithReason:reason];
+
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ // Listen
+
+ status = listen(socketFD, 1024);
+ if (status == -1)
+ {
+ NSString *reason = @"Error in listen() function";
+ err = [self errnoErrorWithReason:reason];
+
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ return socketFD;
+ };
+
+ // Create dispatch block and run on socketQueue
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ if (delegate == nil) // Must have delegate set
+ {
+ NSString *msg = @"Attempting to accept without a delegate. Set a delegate first.";
+ err = [self badConfigError:msg];
+
+ return_from_block;
+ }
+
+ if (delegateQueue == NULL) // Must have delegate queue set
+ {
+ NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first.";
+ err = [self badConfigError:msg];
+
+ return_from_block;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
+ {
+ NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
+ err = [self badConfigError:msg];
+
+ return_from_block;
+ }
+
+ if (![self isDisconnected]) // Must be disconnected
+ {
+ NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first.";
+ err = [self badConfigError:msg];
+
+ return_from_block;
+ }
+
+ // Clear queues (spurious read/write requests post disconnect)
+ [readQueue removeAllObjects];
+ [writeQueue removeAllObjects];
+
+ // Resolve interface from description
+
+ NSMutableData *interface4 = nil;
+ NSMutableData *interface6 = nil;
+
+ [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port];
+
+ if ((interface4 == nil) && (interface6 == nil))
+ {
+ NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ if (isIPv4Disabled && (interface6 == nil))
+ {
+ NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ if (isIPv6Disabled && (interface4 == nil))
+ {
+ NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil);
+ BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil);
+
+ // Create sockets, configure, bind, and listen
+
+ if (enableIPv4)
+ {
+ LogVerbose(@"Creating IPv4 socket");
+ socket4FD = createSocket(AF_INET, interface4);
+
+ if (socket4FD == SOCKET_NULL)
+ {
+ return_from_block;
+ }
+ }
+
+ if (enableIPv6)
+ {
+ LogVerbose(@"Creating IPv6 socket");
+
+ if (enableIPv4 && (port == 0))
+ {
+ // No specific port was specified, so we allowed the OS to pick an available port for us.
+ // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket.
+
+ struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes];
+ addr6->sin6_port = htons([self localPort4]);
+ }
+
+ socket6FD = createSocket(AF_INET6, interface6);
+
+ if (socket6FD == SOCKET_NULL)
+ {
+ if (socket4FD != SOCKET_NULL)
+ {
+ LogVerbose(@"close(socket4FD)");
+ close(socket4FD);
+ }
+
+ return_from_block;
+ }
+ }
+
+ // Create accept sources
+
+ if (enableIPv4)
+ {
+ accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
+
+ int socketFD = socket4FD;
+ dispatch_source_t acceptSource = accept4Source;
+
+ dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"event4Block");
+
+ unsigned long i = 0;
+ unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
+
+ LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
+
+ while ([self doAccept:socketFD] && (++i < numPendingConnections));
+ }});
+
+ dispatch_source_set_cancel_handler(accept4Source, ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(accept4Source)");
+ dispatch_release(acceptSource);
+ #endif
+
+ LogVerbose(@"close(socket4FD)");
+ close(socketFD);
+ });
+
+ LogVerbose(@"dispatch_resume(accept4Source)");
+ dispatch_resume(accept4Source);
+ }
+
+ if (enableIPv6)
+ {
+ accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue);
+
+ int socketFD = socket6FD;
+ dispatch_source_t acceptSource = accept6Source;
+
+ dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"event6Block");
+
+ unsigned long i = 0;
+ unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
+
+ LogVerbose(@"numPendingConnections: %lu", numPendingConnections);
+
+ while ([self doAccept:socketFD] && (++i < numPendingConnections));
+ }});
+
+ dispatch_source_set_cancel_handler(accept6Source, ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(accept6Source)");
+ dispatch_release(acceptSource);
+ #endif
+
+ LogVerbose(@"close(socket6FD)");
+ close(socketFD);
+ });
+
+ LogVerbose(@"dispatch_resume(accept6Source)");
+ dispatch_resume(accept6Source);
+ }
+
+ flags |= kSocketStarted;
+
+ result = YES;
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (result == NO)
+ {
+ LogInfo(@"Error in accept: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+ }
+
+ return result;
+}
+
+- (BOOL)doAccept:(int)parentSocketFD
+{
+ LogTrace();
+
+ BOOL isIPv4;
+ int childSocketFD;
+ NSData *childSocketAddress;
+
+ if (parentSocketFD == socket4FD)
+ {
+ isIPv4 = YES;
+
+ struct sockaddr_in addr;
+ socklen_t addrLen = sizeof(addr);
+
+ childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
+
+ if (childSocketFD == -1)
+ {
+ LogWarn(@"Accept failed with error: %@", [self errnoError]);
+ return NO;
+ }
+
+ childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
+ }
+ else // if (parentSocketFD == socket6FD)
+ {
+ isIPv4 = NO;
+
+ struct sockaddr_in6 addr;
+ socklen_t addrLen = sizeof(addr);
+
+ childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen);
+
+ if (childSocketFD == -1)
+ {
+ LogWarn(@"Accept failed with error: %@", [self errnoError]);
+ return NO;
+ }
+
+ childSocketAddress = [NSData dataWithBytes:&addr length:addrLen];
+ }
+
+ // Enable non-blocking IO on the socket
+
+ int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK);
+ if (result == -1)
+ {
+ LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)");
+ return NO;
+ }
+
+ // Prevent SIGPIPE signals
+
+ int nosigpipe = 1;
+ setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
+
+ // Notify delegate
+
+ if (delegateQueue)
+ {
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ // Query delegate for custom socket queue
+
+ dispatch_queue_t childSocketQueue = NULL;
+
+ if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)])
+ {
+ childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress
+ onSocket:self];
+ }
+
+ // Create GCDAsyncSocket instance for accepted socket
+
+ GCDAsyncSocket *acceptedSocket = [[GCDAsyncSocket alloc] initWithDelegate:theDelegate
+ delegateQueue:delegateQueue
+ socketQueue:childSocketQueue];
+
+ if (isIPv4)
+ acceptedSocket->socket4FD = childSocketFD;
+ else
+ acceptedSocket->socket6FD = childSocketFD;
+
+ acceptedSocket->flags = (kSocketStarted | kConnected);
+
+ // Setup read and write sources for accepted socket
+
+ dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool {
+
+ [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD];
+ }});
+
+ // Notify delegate
+
+ if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)])
+ {
+ [theDelegate socket:self didAcceptNewSocket:acceptedSocket];
+ }
+
+ // Release the socket queue returned from the delegate (it was retained by acceptedSocket)
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (childSocketQueue) dispatch_release(childSocketQueue);
+ #endif
+
+ // The accepted socket should have been retained by the delegate.
+ // Otherwise it gets properly released when exiting the block.
+ }});
+ }
+
+ return YES;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Connecting
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method runs through the various checks required prior to a connection attempt.
+ * It is shared between the connectToHost and connectToAddress methods.
+ *
+**/
+- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if (delegate == nil) // Must have delegate set
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Attempting to connect without a delegate. Set a delegate first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if (delegateQueue == NULL) // Must have delegate queue set
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if (![self isDisconnected]) // Must be disconnected
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if (interface)
+ {
+ NSMutableData *interface4 = nil;
+ NSMutableData *interface6 = nil;
+
+ [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0];
+
+ if ((interface4 == nil) && (interface6 == nil))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
+ *errPtr = [self badParamError:msg];
+ }
+ return NO;
+ }
+
+ if (isIPv4Disabled && (interface6 == nil))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
+ *errPtr = [self badParamError:msg];
+ }
+ return NO;
+ }
+
+ if (isIPv6Disabled && (interface4 == nil))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
+ *errPtr = [self badParamError:msg];
+ }
+ return NO;
+ }
+
+ connectInterface4 = interface4;
+ connectInterface6 = interface6;
+ }
+
+ // Clear queues (spurious read/write requests post disconnect)
+ [readQueue removeAllObjects];
+ [writeQueue removeAllObjects];
+
+ return YES;
+}
+
+- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr
+{
+ return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr];
+}
+
+- (BOOL)connectToHost:(NSString *)host
+ onPort:(uint16_t)port
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr
+{
+ return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr];
+}
+
+- (BOOL)connectToHost:(NSString *)inHost
+ onPort:(uint16_t)port
+ viaInterface:(NSString *)inInterface
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr
+{
+ LogTrace();
+
+ // Just in case immutable objects were passed
+ NSString *host = [inHost copy];
+ NSString *interface = [inInterface copy];
+
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Check for problems with host parameter
+
+ if ([host length] == 0)
+ {
+ NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Run through standard pre-connect checks
+
+ if (![self preConnectWithInterface:interface error:&err])
+ {
+ return_from_block;
+ }
+
+ // We've made it past all the checks.
+ // It's time to start the connection process.
+
+ flags |= kSocketStarted;
+
+ LogVerbose(@"Dispatching DNS lookup...");
+
+ // It's possible that the given host parameter is actually a NSMutableString.
+ // So we want to copy it now, within this block that will be executed synchronously.
+ // This way the asynchronous lookup block below doesn't have to worry about it changing.
+
+ int aConnectIndex = connectIndex;
+ NSString *hostCpy = [host copy];
+
+ dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+ dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
+
+ [self lookup:aConnectIndex host:hostCpy port:port];
+ }});
+
+ [self startConnectTimeout:timeout];
+
+ result = YES;
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (result == NO)
+ {
+ if (errPtr)
+ *errPtr = err;
+ }
+
+ return result;
+}
+
+- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr
+{
+ return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr];
+}
+
+- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr
+{
+ return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr];
+}
+
+- (BOOL)connectToAddress:(NSData *)inRemoteAddr
+ viaInterface:(NSString *)inInterface
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr
+{
+ LogTrace();
+
+ // Just in case immutable objects were passed
+ NSData *remoteAddr = [inRemoteAddr copy];
+ NSString *interface = [inInterface copy];
+
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Check for problems with remoteAddr parameter
+
+ NSData *address4 = nil;
+ NSData *address6 = nil;
+
+ if ([remoteAddr length] >= sizeof(struct sockaddr))
+ {
+ const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes];
+
+ if (sockaddr->sa_family == AF_INET)
+ {
+ if ([remoteAddr length] == sizeof(struct sockaddr_in))
+ {
+ address4 = remoteAddr;
+ }
+ }
+ else if (sockaddr->sa_family == AF_INET6)
+ {
+ if ([remoteAddr length] == sizeof(struct sockaddr_in6))
+ {
+ address6 = remoteAddr;
+ }
+ }
+ }
+
+ if ((address4 == nil) && (address6 == nil))
+ {
+ NSString *msg = @"A valid IPv4 or IPv6 address was not given";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && (address4 != nil))
+ {
+ NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ if (isIPv6Disabled && (address6 != nil))
+ {
+ NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Run through standard pre-connect checks
+
+ if (![self preConnectWithInterface:interface error:&err])
+ {
+ return_from_block;
+ }
+
+ // We've made it past all the checks.
+ // It's time to start the connection process.
+
+ if (![self connectWithAddress4:address4 address6:address6 error:&err])
+ {
+ return_from_block;
+ }
+
+ flags |= kSocketStarted;
+
+ [self startConnectTimeout:timeout];
+
+ result = YES;
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (result == NO)
+ {
+ if (errPtr)
+ *errPtr = err;
+ }
+
+ return result;
+}
+
+- (void)lookup:(int)aConnectIndex host:(NSString *)host port:(uint16_t)port
+{
+ LogTrace();
+
+ // This method is executed on a global concurrent queue.
+ // It posts the results back to the socket queue.
+ // The lookupIndex is used to ignore the results if the connect operation was cancelled or timed out.
+
+ NSError *error = nil;
+
+ NSData *address4 = nil;
+ NSData *address6 = nil;
+
+
+ if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"])
+ {
+ // Use LOOPBACK address
+ struct sockaddr_in nativeAddr;
+ nativeAddr.sin_len = sizeof(struct sockaddr_in);
+ nativeAddr.sin_family = AF_INET;
+ nativeAddr.sin_port = htons(port);
+ nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero));
+
+ struct sockaddr_in6 nativeAddr6;
+ nativeAddr6.sin6_len = sizeof(struct sockaddr_in6);
+ nativeAddr6.sin6_family = AF_INET6;
+ nativeAddr6.sin6_port = htons(port);
+ nativeAddr6.sin6_flowinfo = 0;
+ nativeAddr6.sin6_addr = in6addr_loopback;
+ nativeAddr6.sin6_scope_id = 0;
+
+ // Wrap the native address structures
+ address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)];
+ address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
+ }
+ else
+ {
+ NSString *portStr = [NSString stringWithFormat:@"%hu", port];
+
+ struct addrinfo hints, *res, *res0;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = PF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+
+ int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
+
+ if (gai_error)
+ {
+ error = [self gaiError:gai_error];
+ }
+ else
+ {
+ for(res = res0; res; res = res->ai_next)
+ {
+ if ((address4 == nil) && (res->ai_family == AF_INET))
+ {
+ // Found IPv4 address
+ // Wrap the native address structure
+ address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
+ }
+ else if ((address6 == nil) && (res->ai_family == AF_INET6))
+ {
+ // Found IPv6 address
+ // Wrap the native address structure
+ address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
+ }
+ }
+ freeaddrinfo(res0);
+
+ if ((address4 == nil) && (address6 == nil))
+ {
+ error = [self gaiError:EAI_FAIL];
+ }
+ }
+ }
+
+ if (error)
+ {
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self lookup:aConnectIndex didFail:error];
+ }});
+ }
+ else
+ {
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self lookup:aConnectIndex didSucceedWithAddress4:address4 address6:address6];
+ }});
+ }
+}
+
+- (void)lookup:(int)aConnectIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(address4 || address6, @"Expected at least one valid address");
+
+ if (aConnectIndex != connectIndex)
+ {
+ LogInfo(@"Ignoring lookupDidSucceed, already disconnected");
+
+ // The connect operation has been cancelled.
+ // That is, socket was disconnected, or connection has already timed out.
+ return;
+ }
+
+ // Check for problems
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && (address6 == nil))
+ {
+ NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address.";
+
+ [self closeWithError:[self otherError:msg]];
+ return;
+ }
+
+ if (isIPv6Disabled && (address4 == nil))
+ {
+ NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address.";
+
+ [self closeWithError:[self otherError:msg]];
+ return;
+ }
+
+ // Start the normal connection process
+
+ NSError *err = nil;
+ if (![self connectWithAddress4:address4 address6:address6 error:&err])
+ {
+ [self closeWithError:err];
+ }
+}
+
+/**
+ * This method is called if the DNS lookup fails.
+ * This method is executed on the socketQueue.
+ *
+ * Since the DNS lookup executed synchronously on a global concurrent queue,
+ * the original connection request may have already been cancelled or timed-out by the time this method is invoked.
+ * The lookupIndex tells us whether the lookup is still valid or not.
+**/
+- (void)lookup:(int)aConnectIndex didFail:(NSError *)error
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ if (aConnectIndex != connectIndex)
+ {
+ LogInfo(@"Ignoring lookup:didFail: - already disconnected");
+
+ // The connect operation has been cancelled.
+ // That is, socket was disconnected, or connection has already timed out.
+ return;
+ }
+
+ [self endConnectTimeout];
+ [self closeWithError:error];
+}
+
+- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]);
+ LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]);
+
+ // Determine socket type
+
+ BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
+
+ BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil));
+
+ // Create the socket
+
+ int socketFD;
+ NSData *address;
+ NSData *connectInterface;
+
+ if (useIPv6)
+ {
+ LogVerbose(@"Creating IPv6 socket");
+
+ socket6FD = socket(AF_INET6, SOCK_STREAM, 0);
+
+ socketFD = socket6FD;
+ address = address6;
+ connectInterface = connectInterface6;
+ }
+ else
+ {
+ LogVerbose(@"Creating IPv4 socket");
+
+ socket4FD = socket(AF_INET, SOCK_STREAM, 0);
+
+ socketFD = socket4FD;
+ address = address4;
+ connectInterface = connectInterface4;
+ }
+
+ if (socketFD == SOCKET_NULL)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
+
+ return NO;
+ }
+
+ // Bind the socket to the desired interface (if needed)
+
+ if (connectInterface)
+ {
+ LogVerbose(@"Binding socket...");
+
+ if ([[self class] portFromAddress:connectInterface] > 0)
+ {
+ // Since we're going to be binding to a specific port,
+ // we should turn on reuseaddr to allow us to override sockets in time_wait.
+
+ int reuseOn = 1;
+ setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn));
+ }
+
+ const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes];
+
+ int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]);
+ if (result != 0)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error in bind() function"];
+
+ return NO;
+ }
+ }
+
+ // Prevent SIGPIPE signals
+
+ int nosigpipe = 1;
+ setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
+
+ // Start the connection process in a background queue
+
+ int aConnectIndex = connectIndex;
+
+ dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+ dispatch_async(globalConcurrentQueue, ^{
+
+ int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]);
+ if (result == 0)
+ {
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self didConnect:aConnectIndex];
+ }});
+ }
+ else
+ {
+ NSError *error = [self errnoErrorWithReason:@"Error in connect() function"];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self didNotConnect:aConnectIndex error:error];
+ }});
+ }
+ });
+
+ LogVerbose(@"Connecting...");
+
+ return YES;
+}
+
+- (void)didConnect:(int)aConnectIndex
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ if (aConnectIndex != connectIndex)
+ {
+ LogInfo(@"Ignoring didConnect, already disconnected");
+
+ // The connect operation has been cancelled.
+ // That is, socket was disconnected, or connection has already timed out.
+ return;
+ }
+
+ flags |= kConnected;
+
+ [self endConnectTimeout];
+
+ #if TARGET_OS_IPHONE
+ // The endConnectTimeout method executed above incremented the connectIndex.
+ aConnectIndex = connectIndex;
+ #endif
+
+ // Setup read/write streams (as workaround for specific shortcomings in the iOS platform)
+ //
+ // Note:
+ // There may be configuration options that must be set by the delegate before opening the streams.
+ // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream.
+ //
+ // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed.
+ // This gives the delegate time to properly configure the streams if needed.
+
+ dispatch_block_t SetupStreamsPart1 = ^{
+ #if TARGET_OS_IPHONE
+
+ if (![self createReadAndWriteStream])
+ {
+ [self closeWithError:[self otherError:@"Error creating CFStreams"]];
+ return;
+ }
+
+ if (![self registerForStreamCallbacksIncludingReadWrite:NO])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
+ return;
+ }
+
+ #endif
+ };
+ dispatch_block_t SetupStreamsPart2 = ^{
+ #if TARGET_OS_IPHONE
+
+ if (aConnectIndex != connectIndex)
+ {
+ // The socket has been disconnected.
+ return;
+ }
+
+ if (![self addStreamsToRunLoop])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
+ return;
+ }
+
+ if (![self openStreams])
+ {
+ [self closeWithError:[self otherError:@"Error creating CFStreams"]];
+ return;
+ }
+
+ #endif
+ };
+
+ // Notify delegate
+
+ NSString *host = [self connectedHost];
+ uint16_t port = [self connectedPort];
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:didConnectToHost:port:)])
+ {
+ SetupStreamsPart1();
+
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socket:self didConnectToHost:host port:port];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ SetupStreamsPart2();
+ }});
+ }});
+ }
+ else
+ {
+ SetupStreamsPart1();
+ SetupStreamsPart2();
+ }
+
+ // Get the connected socket
+
+ int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : socket6FD;
+
+ // Enable non-blocking IO on the socket
+
+ int result = fcntl(socketFD, F_SETFL, O_NONBLOCK);
+ if (result == -1)
+ {
+ NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)";
+ [self closeWithError:[self otherError:errMsg]];
+
+ return;
+ }
+
+ // Setup our read/write sources
+
+ [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD];
+
+ // Dequeue any pending read/write requests
+
+ [self maybeDequeueRead];
+ [self maybeDequeueWrite];
+}
+
+- (void)didNotConnect:(int)aConnectIndex error:(NSError *)error
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ if (aConnectIndex != connectIndex)
+ {
+ LogInfo(@"Ignoring didNotConnect, already disconnected");
+
+ // The connect operation has been cancelled.
+ // That is, socket was disconnected, or connection has already timed out.
+ return;
+ }
+
+ [self closeWithError:error];
+}
+
+- (void)startConnectTimeout:(NSTimeInterval)timeout
+{
+ if (timeout >= 0.0)
+ {
+ connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
+
+ dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool {
+
+ [self doConnectTimeout];
+ }});
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theConnectTimer = connectTimer;
+ dispatch_source_set_cancel_handler(connectTimer, ^{
+ LogVerbose(@"dispatch_release(connectTimer)");
+ dispatch_release(theConnectTimer);
+ });
+ #endif
+
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
+ dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0);
+
+ dispatch_resume(connectTimer);
+ }
+}
+
+- (void)endConnectTimeout
+{
+ LogTrace();
+
+ if (connectTimer)
+ {
+ dispatch_source_cancel(connectTimer);
+ connectTimer = NULL;
+ }
+
+ // Increment connectIndex.
+ // This will prevent us from processing results from any related background asynchronous operations.
+ //
+ // Note: This should be called from close method even if connectTimer is NULL.
+ // This is because one might disconnect a socket prior to a successful connection which had no timeout.
+
+ connectIndex++;
+
+ if (connectInterface4)
+ {
+ connectInterface4 = nil;
+ }
+ if (connectInterface6)
+ {
+ connectInterface6 = nil;
+ }
+}
+
+- (void)doConnectTimeout
+{
+ LogTrace();
+
+ [self endConnectTimeout];
+ [self closeWithError:[self connectTimeoutError]];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Disconnecting
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)closeWithError:(NSError *)error
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ [self endConnectTimeout];
+
+ if (currentRead != nil) [self endCurrentRead];
+ if (currentWrite != nil) [self endCurrentWrite];
+
+ [readQueue removeAllObjects];
+ [writeQueue removeAllObjects];
+
+ [preBuffer reset];
+
+ #if TARGET_OS_IPHONE
+ {
+ if (readStream || writeStream)
+ {
+ [self removeStreamsFromRunLoop];
+
+ if (readStream)
+ {
+ CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
+ CFReadStreamClose(readStream);
+ CFRelease(readStream);
+ readStream = NULL;
+ }
+ if (writeStream)
+ {
+ CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL);
+ CFWriteStreamClose(writeStream);
+ CFRelease(writeStream);
+ writeStream = NULL;
+ }
+ }
+ }
+ #endif
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+ {
+ [sslPreBuffer reset];
+ sslErrCode = noErr;
+
+ if (sslContext)
+ {
+ // Getting a linker error here about the SSLx() functions?
+ // You need to add the Security Framework to your application.
+
+ SSLClose(sslContext);
+
+ #if TARGET_OS_IPHONE
+ CFRelease(sslContext);
+ #else
+ SSLDisposeContext(sslContext);
+ #endif
+
+ sslContext = NULL;
+ }
+ }
+ #endif
+
+ // For some crazy reason (in my opinion), cancelling a dispatch source doesn't
+ // invoke the cancel handler if the dispatch source is paused.
+ // So we have to unpause the source if needed.
+ // This allows the cancel handler to be run, which in turn releases the source and closes the socket.
+
+ if (!accept4Source && !accept6Source && !readSource && !writeSource)
+ {
+ LogVerbose(@"manually closing close");
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ LogVerbose(@"close(socket4FD)");
+ close(socket4FD);
+ socket4FD = SOCKET_NULL;
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ LogVerbose(@"close(socket6FD)");
+ close(socket6FD);
+ socket6FD = SOCKET_NULL;
+ }
+ }
+ else
+ {
+ if (accept4Source)
+ {
+ LogVerbose(@"dispatch_source_cancel(accept4Source)");
+ dispatch_source_cancel(accept4Source);
+
+ // We never suspend accept4Source
+
+ accept4Source = NULL;
+ }
+
+ if (accept6Source)
+ {
+ LogVerbose(@"dispatch_source_cancel(accept6Source)");
+ dispatch_source_cancel(accept6Source);
+
+ // We never suspend accept6Source
+
+ accept6Source = NULL;
+ }
+
+ if (readSource)
+ {
+ LogVerbose(@"dispatch_source_cancel(readSource)");
+ dispatch_source_cancel(readSource);
+
+ [self resumeReadSource];
+
+ readSource = NULL;
+ }
+
+ if (writeSource)
+ {
+ LogVerbose(@"dispatch_source_cancel(writeSource)");
+ dispatch_source_cancel(writeSource);
+
+ [self resumeWriteSource];
+
+ writeSource = NULL;
+ }
+
+ // The sockets will be closed by the cancel handlers of the corresponding source
+
+ socket4FD = SOCKET_NULL;
+ socket6FD = SOCKET_NULL;
+ }
+
+ // If the client has passed the connect/accept method, then the connection has at least begun.
+ // Notify delegate that it is now ending.
+ BOOL shouldCallDelegate = (flags & kSocketStarted);
+
+ // Clear stored socket info and all flags (config remains as is)
+ socketFDBytesAvailable = 0;
+ flags = 0;
+
+ if (shouldCallDelegate)
+ {
+ if (delegateQueue && [delegate respondsToSelector: @selector(socketDidDisconnect:withError:)])
+ {
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socketDidDisconnect:self withError:error];
+ }});
+ }
+ }
+}
+
+- (void)disconnect
+{
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ if (flags & kSocketStarted)
+ {
+ [self closeWithError:nil];
+ }
+ }};
+
+ // Synchronous disconnection, as documented in the header file
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+}
+
+- (void)disconnectAfterReading
+{
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ if (flags & kSocketStarted)
+ {
+ flags |= (kForbidReadsWrites | kDisconnectAfterReads);
+ [self maybeClose];
+ }
+ }});
+}
+
+- (void)disconnectAfterWriting
+{
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ if (flags & kSocketStarted)
+ {
+ flags |= (kForbidReadsWrites | kDisconnectAfterWrites);
+ [self maybeClose];
+ }
+ }});
+}
+
+- (void)disconnectAfterReadingAndWriting
+{
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ if (flags & kSocketStarted)
+ {
+ flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites);
+ [self maybeClose];
+ }
+ }});
+}
+
+/**
+ * Closes the socket if possible.
+ * That is, if all writes have completed, and we're set to disconnect after writing,
+ * or if all reads have completed, and we're set to disconnect after reading.
+**/
+- (void)maybeClose
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ BOOL shouldClose = NO;
+
+ if (flags & kDisconnectAfterReads)
+ {
+ if (([readQueue count] == 0) && (currentRead == nil))
+ {
+ if (flags & kDisconnectAfterWrites)
+ {
+ if (([writeQueue count] == 0) && (currentWrite == nil))
+ {
+ shouldClose = YES;
+ }
+ }
+ else
+ {
+ shouldClose = YES;
+ }
+ }
+ }
+ else if (flags & kDisconnectAfterWrites)
+ {
+ if (([writeQueue count] == 0) && (currentWrite == nil))
+ {
+ shouldClose = YES;
+ }
+ }
+
+ if (shouldClose)
+ {
+ [self closeWithError:nil];
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Errors
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (NSError *)badConfigError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo];
+}
+
+- (NSError *)badParamError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo];
+}
+
+- (NSError *)gaiError:(int)gai_error
+{
+ NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding];
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo];
+}
+
+- (NSError *)errnoErrorWithReason:(NSString *)reason
+{
+ NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)];
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey,
+ reason, NSLocalizedFailureReasonErrorKey, nil];
+
+ return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo];
+}
+
+- (NSError *)errnoError
+{
+ NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)];
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo];
+}
+
+- (NSError *)sslError:(OSStatus)ssl_error
+{
+ NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h";
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey];
+
+ return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo];
+}
+
+- (NSError *)connectTimeoutError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError",
+ @"GCDAsyncSocket", [NSBundle mainBundle],
+ @"Attempt to connect to host timed out", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo];
+}
+
+/**
+ * Returns a standard AsyncSocket maxed out error.
+**/
+- (NSError *)readMaxedOutError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError",
+ @"GCDAsyncSocket", [NSBundle mainBundle],
+ @"Read operation reached set maximum length", nil);
+
+ NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info];
+}
+
+/**
+ * Returns a standard AsyncSocket write timeout error.
+**/
+- (NSError *)readTimeoutError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError",
+ @"GCDAsyncSocket", [NSBundle mainBundle],
+ @"Read operation timed out", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo];
+}
+
+/**
+ * Returns a standard AsyncSocket write timeout error.
+**/
+- (NSError *)writeTimeoutError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError",
+ @"GCDAsyncSocket", [NSBundle mainBundle],
+ @"Write operation timed out", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo];
+}
+
+- (NSError *)connectionClosedError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError",
+ @"GCDAsyncSocket", [NSBundle mainBundle],
+ @"Socket closed by remote peer", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo];
+}
+
+- (NSError *)otherError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Diagnostics
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)isDisconnected
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (flags & kSocketStarted) ? NO : YES;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isConnected
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (flags & kConnected) ? YES : NO;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (NSString *)connectedHost
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (socket4FD != SOCKET_NULL)
+ return [self connectedHostFromSocket4:socket4FD];
+ if (socket6FD != SOCKET_NULL)
+ return [self connectedHostFromSocket6:socket6FD];
+
+ return nil;
+ }
+ else
+ {
+ __block NSString *result = nil;
+
+ dispatch_sync(socketQueue, ^{ @autoreleasepool {
+
+ if (socket4FD != SOCKET_NULL)
+ result = [self connectedHostFromSocket4:socket4FD];
+ else if (socket6FD != SOCKET_NULL)
+ result = [self connectedHostFromSocket6:socket6FD];
+ }});
+
+ return result;
+ }
+}
+
+- (uint16_t)connectedPort
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (socket4FD != SOCKET_NULL)
+ return [self connectedPortFromSocket4:socket4FD];
+ if (socket6FD != SOCKET_NULL)
+ return [self connectedPortFromSocket6:socket6FD];
+
+ return 0;
+ }
+ else
+ {
+ __block uint16_t result = 0;
+
+ dispatch_sync(socketQueue, ^{
+ // No need for autorelease pool
+
+ if (socket4FD != SOCKET_NULL)
+ result = [self connectedPortFromSocket4:socket4FD];
+ else if (socket6FD != SOCKET_NULL)
+ result = [self connectedPortFromSocket6:socket6FD];
+ });
+
+ return result;
+ }
+}
+
+- (NSString *)localHost
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (socket4FD != SOCKET_NULL)
+ return [self localHostFromSocket4:socket4FD];
+ if (socket6FD != SOCKET_NULL)
+ return [self localHostFromSocket6:socket6FD];
+
+ return nil;
+ }
+ else
+ {
+ __block NSString *result = nil;
+
+ dispatch_sync(socketQueue, ^{ @autoreleasepool {
+
+ if (socket4FD != SOCKET_NULL)
+ result = [self localHostFromSocket4:socket4FD];
+ else if (socket6FD != SOCKET_NULL)
+ result = [self localHostFromSocket6:socket6FD];
+ }});
+
+ return result;
+ }
+}
+
+- (uint16_t)localPort
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (socket4FD != SOCKET_NULL)
+ return [self localPortFromSocket4:socket4FD];
+ if (socket6FD != SOCKET_NULL)
+ return [self localPortFromSocket6:socket6FD];
+
+ return 0;
+ }
+ else
+ {
+ __block uint16_t result = 0;
+
+ dispatch_sync(socketQueue, ^{
+ // No need for autorelease pool
+
+ if (socket4FD != SOCKET_NULL)
+ result = [self localPortFromSocket4:socket4FD];
+ else if (socket6FD != SOCKET_NULL)
+ result = [self localPortFromSocket6:socket6FD];
+ });
+
+ return result;
+ }
+}
+
+- (NSString *)connectedHost4
+{
+ if (socket4FD != SOCKET_NULL)
+ return [self connectedHostFromSocket4:socket4FD];
+
+ return nil;
+}
+
+- (NSString *)connectedHost6
+{
+ if (socket6FD != SOCKET_NULL)
+ return [self connectedHostFromSocket6:socket6FD];
+
+ return nil;
+}
+
+- (uint16_t)connectedPort4
+{
+ if (socket4FD != SOCKET_NULL)
+ return [self connectedPortFromSocket4:socket4FD];
+
+ return 0;
+}
+
+- (uint16_t)connectedPort6
+{
+ if (socket6FD != SOCKET_NULL)
+ return [self connectedPortFromSocket6:socket6FD];
+
+ return 0;
+}
+
+- (NSString *)localHost4
+{
+ if (socket4FD != SOCKET_NULL)
+ return [self localHostFromSocket4:socket4FD];
+
+ return nil;
+}
+
+- (NSString *)localHost6
+{
+ if (socket6FD != SOCKET_NULL)
+ return [self localHostFromSocket6:socket6FD];
+
+ return nil;
+}
+
+- (uint16_t)localPort4
+{
+ if (socket4FD != SOCKET_NULL)
+ return [self localPortFromSocket4:socket4FD];
+
+ return 0;
+}
+
+- (uint16_t)localPort6
+{
+ if (socket6FD != SOCKET_NULL)
+ return [self localPortFromSocket6:socket6FD];
+
+ return 0;
+}
+
+- (NSString *)connectedHostFromSocket4:(int)socketFD
+{
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
+ {
+ return nil;
+ }
+ return [[self class] hostFromSockaddr4:&sockaddr4];
+}
+
+- (NSString *)connectedHostFromSocket6:(int)socketFD
+{
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
+ {
+ return nil;
+ }
+ return [[self class] hostFromSockaddr6:&sockaddr6];
+}
+
+- (uint16_t)connectedPortFromSocket4:(int)socketFD
+{
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
+ {
+ return 0;
+ }
+ return [[self class] portFromSockaddr4:&sockaddr4];
+}
+
+- (uint16_t)connectedPortFromSocket6:(int)socketFD
+{
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
+ {
+ return 0;
+ }
+ return [[self class] portFromSockaddr6:&sockaddr6];
+}
+
+- (NSString *)localHostFromSocket4:(int)socketFD
+{
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
+ {
+ return nil;
+ }
+ return [[self class] hostFromSockaddr4:&sockaddr4];
+}
+
+- (NSString *)localHostFromSocket6:(int)socketFD
+{
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
+ {
+ return nil;
+ }
+ return [[self class] hostFromSockaddr6:&sockaddr6];
+}
+
+- (uint16_t)localPortFromSocket4:(int)socketFD
+{
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0)
+ {
+ return 0;
+ }
+ return [[self class] portFromSockaddr4:&sockaddr4];
+}
+
+- (uint16_t)localPortFromSocket6:(int)socketFD
+{
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0)
+ {
+ return 0;
+ }
+ return [[self class] portFromSockaddr6:&sockaddr6];
+}
+
+- (NSData *)connectedAddress
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+ if (socket4FD != SOCKET_NULL)
+ {
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
+ {
+ result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len];
+ }
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
+ {
+ result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len];
+ }
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (NSData *)localAddress
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+ if (socket4FD != SOCKET_NULL)
+ {
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
+ {
+ result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len];
+ }
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
+ {
+ result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len];
+ }
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isIPv4
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return (socket4FD != SOCKET_NULL);
+ }
+ else
+ {
+ __block BOOL result = NO;
+
+ dispatch_sync(socketQueue, ^{
+ result = (socket4FD != SOCKET_NULL);
+ });
+
+ return result;
+ }
+}
+
+- (BOOL)isIPv6
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return (socket6FD != SOCKET_NULL);
+ }
+ else
+ {
+ __block BOOL result = NO;
+
+ dispatch_sync(socketQueue, ^{
+ result = (socket6FD != SOCKET_NULL);
+ });
+
+ return result;
+ }
+}
+
+- (BOOL)isSecure
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return (flags & kSocketSecure) ? YES : NO;
+ }
+ else
+ {
+ __block BOOL result;
+
+ dispatch_sync(socketQueue, ^{
+ result = (flags & kSocketSecure) ? YES : NO;
+ });
+
+ return result;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Utilities
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Finds the address of an interface description.
+ * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34).
+ *
+ * The interface description may optionally contain a port number at the end, separated by a colon.
+ * If a non-zero port parameter is provided, any port number in the interface description is ignored.
+ *
+ * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object.
+**/
+- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr
+ address6:(NSMutableData **)interfaceAddr6Ptr
+ fromDescription:(NSString *)interfaceDescription
+ port:(uint16_t)port
+{
+ NSMutableData *addr4 = nil;
+ NSMutableData *addr6 = nil;
+
+ NSString *interface = nil;
+
+ NSArray *components = [interfaceDescription componentsSeparatedByString:@":"];
+ if ([components count] > 0)
+ {
+ NSString *temp = [components objectAtIndex:0];
+ if ([temp length] > 0)
+ {
+ interface = temp;
+ }
+ }
+ if ([components count] > 1 && port == 0)
+ {
+ long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10);
+
+ if (portL > 0 && portL <= UINT16_MAX)
+ {
+ port = (uint16_t)portL;
+ }
+ }
+
+ if (interface == nil)
+ {
+ // ANY address
+
+ struct sockaddr_in sockaddr4;
+ memset(&sockaddr4, 0, sizeof(sockaddr4));
+
+ sockaddr4.sin_len = sizeof(sockaddr4);
+ sockaddr4.sin_family = AF_INET;
+ sockaddr4.sin_port = htons(port);
+ sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ struct sockaddr_in6 sockaddr6;
+ memset(&sockaddr6, 0, sizeof(sockaddr6));
+
+ sockaddr6.sin6_len = sizeof(sockaddr6);
+ sockaddr6.sin6_family = AF_INET6;
+ sockaddr6.sin6_port = htons(port);
+ sockaddr6.sin6_addr = in6addr_any;
+
+ addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
+ addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
+ }
+ else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"])
+ {
+ // LOOPBACK address
+
+ struct sockaddr_in sockaddr4;
+ memset(&sockaddr4, 0, sizeof(sockaddr4));
+
+ sockaddr4.sin_len = sizeof(sockaddr4);
+ sockaddr4.sin_family = AF_INET;
+ sockaddr4.sin_port = htons(port);
+ sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ struct sockaddr_in6 sockaddr6;
+ memset(&sockaddr6, 0, sizeof(sockaddr6));
+
+ sockaddr6.sin6_len = sizeof(sockaddr6);
+ sockaddr6.sin6_family = AF_INET6;
+ sockaddr6.sin6_port = htons(port);
+ sockaddr6.sin6_addr = in6addr_loopback;
+
+ addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
+ addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
+ }
+ else
+ {
+ const char *iface = [interface UTF8String];
+
+ struct ifaddrs *addrs;
+ const struct ifaddrs *cursor;
+
+ if ((getifaddrs(&addrs) == 0))
+ {
+ cursor = addrs;
+ while (cursor != NULL)
+ {
+ if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
+ {
+ // IPv4
+
+ struct sockaddr_in nativeAddr4;
+ memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4));
+
+ if (strcmp(cursor->ifa_name, iface) == 0)
+ {
+ // Name match
+
+ nativeAddr4.sin_port = htons(port);
+
+ addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
+ }
+ else
+ {
+ char ip[INET_ADDRSTRLEN];
+
+ const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip));
+
+ if ((conversion != NULL) && (strcmp(ip, iface) == 0))
+ {
+ // IP match
+
+ nativeAddr4.sin_port = htons(port);
+
+ addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
+ }
+ }
+ }
+ else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6))
+ {
+ // IPv6
+
+ struct sockaddr_in6 nativeAddr6;
+ memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6));
+
+ if (strcmp(cursor->ifa_name, iface) == 0)
+ {
+ // Name match
+
+ nativeAddr6.sin6_port = htons(port);
+
+ addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
+ }
+ else
+ {
+ char ip[INET6_ADDRSTRLEN];
+
+ const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip));
+
+ if ((conversion != NULL) && (strcmp(ip, iface) == 0))
+ {
+ // IP match
+
+ nativeAddr6.sin6_port = htons(port);
+
+ addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
+ }
+ }
+ }
+
+ cursor = cursor->ifa_next;
+ }
+
+ freeifaddrs(addrs);
+ }
+ }
+
+ if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
+ if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;
+}
+
+- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD
+{
+ readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue);
+ writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue);
+
+ // Setup event handlers
+
+ dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool {
+
+ LogVerbose(@"readEventBlock");
+
+ socketFDBytesAvailable = dispatch_source_get_data(readSource);
+ LogVerbose(@"socketFDBytesAvailable: %lu", socketFDBytesAvailable);
+
+ if (socketFDBytesAvailable > 0)
+ [self doReadData];
+ else
+ [self doReadEOF];
+ }});
+
+ dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool {
+
+ LogVerbose(@"writeEventBlock");
+
+ flags |= kSocketCanAcceptBytes;
+ [self doWriteData];
+ }});
+
+ // Setup cancel handlers
+
+ __block int socketFDRefCount = 2;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theReadSource = readSource;
+ dispatch_source_t theWriteSource = writeSource;
+ #endif
+
+ dispatch_source_set_cancel_handler(readSource, ^{
+
+ LogVerbose(@"readCancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(readSource)");
+ dispatch_release(theReadSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ }
+ });
+
+ dispatch_source_set_cancel_handler(writeSource, ^{
+
+ LogVerbose(@"writeCancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(writeSource)");
+ dispatch_release(theWriteSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socketFD)");
+ close(socketFD);
+ }
+ });
+
+ // We will not be able to read until data arrives.
+ // But we should be able to write immediately.
+
+ socketFDBytesAvailable = 0;
+ flags &= ~kReadSourceSuspended;
+
+ LogVerbose(@"dispatch_resume(readSource)");
+ dispatch_resume(readSource);
+
+ flags |= kSocketCanAcceptBytes;
+ flags |= kWriteSourceSuspended;
+}
+
+- (BOOL)usingCFStreamForTLS
+{
+ #if TARGET_OS_IPHONE
+ {
+ if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS))
+ {
+ // Due to the fact that Apple doesn't give us the full power of SecureTransport on iOS,
+ // we are relegated to using the slower, less powerful, and RunLoop based CFStream API. :( Boo!
+ //
+ // Thus we're not able to use the GCD read/write sources in this particular scenario.
+
+ return YES;
+ }
+ }
+ #endif
+
+ return NO;
+}
+
+- (BOOL)usingSecureTransportForTLS
+{
+ #if TARGET_OS_IPHONE
+ {
+ return ![self usingCFStreamForTLS];
+ }
+ #endif
+
+ return YES;
+}
+
+- (void)suspendReadSource
+{
+ if (!(flags & kReadSourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(readSource)");
+
+ dispatch_suspend(readSource);
+ flags |= kReadSourceSuspended;
+ }
+}
+
+- (void)resumeReadSource
+{
+ if (flags & kReadSourceSuspended)
+ {
+ LogVerbose(@"dispatch_resume(readSource)");
+
+ dispatch_resume(readSource);
+ flags &= ~kReadSourceSuspended;
+ }
+}
+
+- (void)suspendWriteSource
+{
+ if (!(flags & kWriteSourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(writeSource)");
+
+ dispatch_suspend(writeSource);
+ flags |= kWriteSourceSuspended;
+ }
+}
+
+- (void)resumeWriteSource
+{
+ if (flags & kWriteSourceSuspended)
+ {
+ LogVerbose(@"dispatch_resume(writeSource)");
+
+ dispatch_resume(writeSource);
+ flags &= ~kWriteSourceSuspended;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Reading
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
+}
+
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag
+{
+ [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
+}
+
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)length
+ tag:(long)tag
+{
+ if (offset > [buffer length]) {
+ LogWarn(@"Cannot read: offset > [buffer length]");
+ return;
+ }
+
+ GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
+ startOffset:offset
+ maxLength:length
+ timeout:timeout
+ readLength:0
+ terminator:nil
+ tag:tag];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ LogTrace();
+
+ if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
+ {
+ [readQueue addObject:packet];
+ [self maybeDequeueRead];
+ }
+ }});
+
+ // Do not rely on the block being run in order to release the packet,
+ // as the queue might get released without the block completing.
+}
+
+- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag];
+}
+
+- (void)readDataToLength:(NSUInteger)length
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag
+{
+ if (length == 0) {
+ LogWarn(@"Cannot read: length == 0");
+ return;
+ }
+ if (offset > [buffer length]) {
+ LogWarn(@"Cannot read: offset > [buffer length]");
+ return;
+ }
+
+ GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
+ startOffset:offset
+ maxLength:0
+ timeout:timeout
+ readLength:length
+ terminator:nil
+ tag:tag];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ LogTrace();
+
+ if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
+ {
+ [readQueue addObject:packet];
+ [self maybeDequeueRead];
+ }
+ }});
+
+ // Do not rely on the block being run in order to release the packet,
+ // as the queue might get released without the block completing.
+}
+
+- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
+}
+
+- (void)readDataToData:(NSData *)data
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag
+{
+ [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
+}
+
+- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag
+{
+ [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag];
+}
+
+- (void)readDataToData:(NSData *)data
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)maxLength
+ tag:(long)tag
+{
+ if ([data length] == 0) {
+ LogWarn(@"Cannot read: [data length] == 0");
+ return;
+ }
+ if (offset > [buffer length]) {
+ LogWarn(@"Cannot read: offset > [buffer length]");
+ return;
+ }
+ if (maxLength > 0 && maxLength < [data length]) {
+ LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]");
+ return;
+ }
+
+ GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
+ startOffset:offset
+ maxLength:maxLength
+ timeout:timeout
+ readLength:0
+ terminator:data
+ tag:tag];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ LogTrace();
+
+ if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
+ {
+ [readQueue addObject:packet];
+ [self maybeDequeueRead];
+ }
+ }});
+
+ // Do not rely on the block being run in order to release the packet,
+ // as the queue might get released without the block completing.
+}
+
+- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr
+{
+ __block float result = 0.0F;
+
+ dispatch_block_t block = ^{
+
+ if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]])
+ {
+ // We're not reading anything right now.
+
+ if (tagPtr != NULL) *tagPtr = 0;
+ if (donePtr != NULL) *donePtr = 0;
+ if (totalPtr != NULL) *totalPtr = 0;
+
+ result = NAN;
+ }
+ else
+ {
+ // It's only possible to know the progress of our read if we're reading to a certain length.
+ // If we're reading to data, we of course have no idea when the data will arrive.
+ // If we're reading to timeout, then we have no idea when the next chunk of data will arrive.
+
+ NSUInteger done = currentRead->bytesDone;
+ NSUInteger total = currentRead->readLength;
+
+ if (tagPtr != NULL) *tagPtr = currentRead->tag;
+ if (donePtr != NULL) *donePtr = done;
+ if (totalPtr != NULL) *totalPtr = total;
+
+ if (total > 0)
+ result = (float)done / (float)total;
+ else
+ result = 1.0F;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+/**
+ * This method starts a new read, if needed.
+ *
+ * It is called when:
+ * - a user requests a read
+ * - after a read request has finished (to handle the next request)
+ * - immediately after the socket opens to handle any pending requests
+ *
+ * This method also handles auto-disconnect post read/write completion.
+**/
+- (void)maybeDequeueRead
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ // If we're not currently processing a read AND we have an available read stream
+ if ((currentRead == nil) && (flags & kConnected))
+ {
+ if ([readQueue count] > 0)
+ {
+ // Dequeue the next object in the write queue
+ currentRead = [readQueue objectAtIndex:0];
+ [readQueue removeObjectAtIndex:0];
+
+
+ if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
+ {
+ LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
+
+ // Attempt to start TLS
+ flags |= kStartingReadTLS;
+
+ // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
+ [self maybeStartTLS];
+ }
+ else
+ {
+ LogVerbose(@"Dequeued GCDAsyncReadPacket");
+
+ // Setup read timer (if needed)
+ [self setupReadTimerWithTimeout:currentRead->timeout];
+
+ // Immediately read, if possible
+ [self doReadData];
+ }
+ }
+ else if (flags & kDisconnectAfterReads)
+ {
+ if (flags & kDisconnectAfterWrites)
+ {
+ if (([writeQueue count] == 0) && (currentWrite == nil))
+ {
+ [self closeWithError:nil];
+ }
+ }
+ else
+ {
+ [self closeWithError:nil];
+ }
+ }
+ else if (flags & kSocketSecure)
+ {
+ [self flushSSLBuffers];
+
+ // Edge case:
+ //
+ // We just drained all data from the ssl buffers,
+ // and all known data from the socket (socketFDBytesAvailable).
+ //
+ // If we didn't get any data from this process,
+ // then we may have reached the end of the TCP stream.
+ //
+ // Be sure callbacks are enabled so we're notified about a disconnection.
+
+ if ([preBuffer availableBytes] == 0)
+ {
+ if ([self usingCFStreamForTLS]) {
+ // Callbacks never disabled
+ }
+ else {
+ [self resumeReadSource];
+ }
+ }
+ }
+ }
+}
+
+- (void)flushSSLBuffers
+{
+ LogTrace();
+
+ NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket");
+
+ if ([preBuffer availableBytes] > 0)
+ {
+ // Only flush the ssl buffers if the prebuffer is empty.
+ // This is to avoid growing the prebuffer inifinitely large.
+
+ return;
+ }
+
+#if TARGET_OS_IPHONE
+
+ if ([self usingCFStreamForTLS])
+ {
+ if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
+ {
+ LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);
+
+ CFIndex defaultBytesToRead = (1024 * 4);
+
+ [preBuffer ensureCapacityForWrite:defaultBytesToRead];
+
+ uint8_t *buffer = [preBuffer writeBuffer];
+
+ CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
+ LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result);
+
+ if (result > 0)
+ {
+ [preBuffer didWrite:result];
+ }
+
+ flags &= ~kSecureSocketHasBytesAvailable;
+ }
+
+ return;
+ }
+
+#endif
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ __block NSUInteger estimatedBytesAvailable = 0;
+
+ dispatch_block_t updateEstimatedBytesAvailable = ^{
+
+ // Figure out if there is any data available to be read
+ //
+ // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket
+ // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket
+ // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered
+ //
+ // We call the variable "estimated" because we don't know how many decrypted bytes we'll get
+ // from the encrypted bytes in the sslPreBuffer.
+ // However, we do know this is an upper bound on the estimation.
+
+ estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes];
+
+ size_t sslInternalBufSize = 0;
+ SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
+
+ estimatedBytesAvailable += sslInternalBufSize;
+ };
+
+ updateEstimatedBytesAvailable();
+
+ if (estimatedBytesAvailable > 0)
+ {
+ LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);
+
+ BOOL done = NO;
+ do
+ {
+ LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable);
+
+ // Make sure there's enough room in the prebuffer
+
+ [preBuffer ensureCapacityForWrite:estimatedBytesAvailable];
+
+ // Read data into prebuffer
+
+ uint8_t *buffer = [preBuffer writeBuffer];
+ size_t bytesRead = 0;
+
+ OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
+ LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead);
+
+ if (bytesRead > 0)
+ {
+ [preBuffer didWrite:bytesRead];
+ }
+
+ LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]);
+
+ if (result != noErr)
+ {
+ done = YES;
+ }
+ else
+ {
+ updateEstimatedBytesAvailable();
+ }
+
+ } while (!done && estimatedBytesAvailable > 0);
+ }
+
+#endif
+}
+
+- (void)doReadData
+{
+ LogTrace();
+
+ // This method is called on the socketQueue.
+ // It might be called directly, or via the readSource when data is available to be read.
+
+ if ((currentRead == nil) || (flags & kReadsPaused))
+ {
+ LogVerbose(@"No currentRead or kReadsPaused");
+
+ // Unable to read at this time
+
+ if (flags & kSocketSecure)
+ {
+ // Here's the situation:
+ //
+ // We have an established secure connection.
+ // There may not be a currentRead, but there might be encrypted data sitting around for us.
+ // When the user does get around to issuing a read, that encrypted data will need to be decrypted.
+ //
+ // So why make the user wait?
+ // We might as well get a head start on decrypting some data now.
+ //
+ // The other reason we do this has to do with detecting a socket disconnection.
+ // The SSL/TLS protocol has it's own disconnection handshake.
+ // So when a secure socket is closed, a "goodbye" packet comes across the wire.
+ // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection.
+
+ [self flushSSLBuffers];
+ }
+
+ if ([self usingCFStreamForTLS])
+ {
+ // CFReadStream only fires once when there is available data.
+ // It won't fire again until we've invoked CFReadStreamRead.
+ }
+ else
+ {
+ // If the readSource is firing, we need to pause it
+ // or else it will continue to fire over and over again.
+ //
+ // If the readSource is not firing,
+ // we want it to continue monitoring the socket.
+
+ if (socketFDBytesAvailable > 0)
+ {
+ [self suspendReadSource];
+ }
+ }
+ return;
+ }
+
+ BOOL hasBytesAvailable;
+ unsigned long estimatedBytesAvailable;
+
+ if ([self usingCFStreamForTLS])
+ {
+ #if TARGET_OS_IPHONE
+
+ // Relegated to using CFStream... :( Boo! Give us a full SecureTransport stack Apple!
+
+ estimatedBytesAvailable = 0;
+ if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
+ hasBytesAvailable = YES;
+ else
+ hasBytesAvailable = NO;
+
+ #endif
+ }
+ else
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ estimatedBytesAvailable = socketFDBytesAvailable;
+
+ if (flags & kSocketSecure)
+ {
+ // There are 2 buffers to be aware of here.
+ //
+ // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP.
+ // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction.
+ // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport.
+ // SecureTransport then decrypts the data, and finally returns the decrypted data back to us.
+ //
+ // The first buffer is one we create.
+ // SecureTransport often requests small amounts of data.
+ // This has to do with the encypted packets that are coming across the TCP stream.
+ // But it's non-optimal to do a bunch of small reads from the BSD socket.
+ // So our SSLReadFunction reads all available data from the socket (optimizing the sys call)
+ // and may store excess in the sslPreBuffer.
+
+ estimatedBytesAvailable += [sslPreBuffer availableBytes];
+
+ // The second buffer is within SecureTransport.
+ // As mentioned earlier, there are encrypted packets coming across the TCP stream.
+ // SecureTransport needs the entire packet to decrypt it.
+ // But if the entire packet produces X bytes of decrypted data,
+ // and we only asked SecureTransport for X/2 bytes of data,
+ // it must store the extra X/2 bytes of decrypted data for the next read.
+ //
+ // The SSLGetBufferedReadSize function will tell us the size of this internal buffer.
+ // From the documentation:
+ //
+ // "This function does not block or cause any low-level read operations to occur."
+
+ size_t sslInternalBufSize = 0;
+ SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
+
+ estimatedBytesAvailable += sslInternalBufSize;
+ }
+
+ hasBytesAvailable = (estimatedBytesAvailable > 0);
+
+ #endif
+ }
+
+ if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0))
+ {
+ LogVerbose(@"No data available to read...");
+
+ // No data available to read.
+
+ if (![self usingCFStreamForTLS])
+ {
+ // Need to wait for readSource to fire and notify us of
+ // available data in the socket's internal read buffer.
+
+ [self resumeReadSource];
+ }
+ return;
+ }
+
+ if (flags & kStartingReadTLS)
+ {
+ LogVerbose(@"Waiting for SSL/TLS handshake to complete");
+
+ // The readQueue is waiting for SSL/TLS handshake to complete.
+
+ if (flags & kStartingWriteTLS)
+ {
+ if ([self usingSecureTransportForTLS])
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ // We are in the process of a SSL Handshake.
+ // We were waiting for incoming data which has just arrived.
+
+ [self ssl_continueSSLHandshake];
+
+ #endif
+ }
+ }
+ else
+ {
+ // We are still waiting for the writeQueue to drain and start the SSL/TLS process.
+ // We now know data is available to read.
+
+ if (![self usingCFStreamForTLS])
+ {
+ // Suspend the read source or else it will continue to fire nonstop.
+
+ [self suspendReadSource];
+ }
+ }
+
+ return;
+ }
+
+ BOOL done = NO; // Completed read operation
+ NSError *error = nil; // Error occured
+
+ NSUInteger totalBytesReadForCurrentRead = 0;
+
+ //
+ // STEP 1 - READ FROM PREBUFFER
+ //
+
+ if ([preBuffer availableBytes] > 0)
+ {
+ // There are 3 types of read packets:
+ //
+ // 1) Read all available data.
+ // 2) Read a specific length of data.
+ // 3) Read up to a particular terminator.
+
+ NSUInteger bytesToCopy;
+
+ if (currentRead->term != nil)
+ {
+ // Read type #3 - read up to a terminator
+
+ bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
+ }
+ else
+ {
+ // Read type #1 or #2
+
+ bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
+ }
+
+ // Make sure we have enough room in the buffer for our read.
+
+ [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];
+
+ // Copy bytes from prebuffer into packet buffer
+
+ uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset +
+ currentRead->bytesDone;
+
+ memcpy(buffer, [preBuffer readBuffer], bytesToCopy);
+
+ // Remove the copied bytes from the preBuffer
+ [preBuffer didRead:bytesToCopy];
+
+ LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]);
+
+ // Update totals
+
+ currentRead->bytesDone += bytesToCopy;
+ totalBytesReadForCurrentRead += bytesToCopy;
+
+ // Check to see if the read operation is done
+
+ if (currentRead->readLength > 0)
+ {
+ // Read type #2 - read a specific length of data
+
+ done = (currentRead->bytesDone == currentRead->readLength);
+ }
+ else if (currentRead->term != nil)
+ {
+ // Read type #3 - read up to a terminator
+
+ // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method
+
+ if (!done && currentRead->maxLength > 0)
+ {
+ // We're not done and there's a set maxLength.
+ // Have we reached that maxLength yet?
+
+ if (currentRead->bytesDone >= currentRead->maxLength)
+ {
+ error = [self readMaxedOutError];
+ }
+ }
+ }
+ else
+ {
+ // Read type #1 - read all available data
+ //
+ // We're done as soon as
+ // - we've read all available data (in prebuffer and socket)
+ // - we've read the maxLength of read packet.
+
+ done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength));
+ }
+
+ }
+
+ //
+ // STEP 2 - READ FROM SOCKET
+ //
+
+ BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to via socket (end of file)
+ BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more
+
+ if (!done && !error && !socketEOF && !waiting && hasBytesAvailable)
+ {
+ NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic");
+
+ // There are 3 types of read packets:
+ //
+ // 1) Read all available data.
+ // 2) Read a specific length of data.
+ // 3) Read up to a particular terminator.
+
+ BOOL readIntoPreBuffer = NO;
+ NSUInteger bytesToRead;
+
+ if ([self usingCFStreamForTLS])
+ {
+ // Since Apple hasn't made the full power of SecureTransport available on iOS,
+ // we are relegated to using the slower, less powerful, RunLoop based CFStream API.
+ //
+ // This API doesn't tell us how much data is available on the socket to be read.
+ // If we had that information we could optimize our memory allocations, and sys calls.
+ //
+ // But alas...
+ // So we do it old school, and just read as much data from the socket as we can.
+
+ NSUInteger defaultReadLength = (1024 * 32);
+
+ bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength
+ shouldPreBuffer:&readIntoPreBuffer];
+ }
+ else
+ {
+ if (currentRead->term != nil)
+ {
+ // Read type #3 - read up to a terminator
+
+ bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable
+ shouldPreBuffer:&readIntoPreBuffer];
+ }
+ else
+ {
+ // Read type #1 or #2
+
+ bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable];
+ }
+ }
+
+ if (bytesToRead > SIZE_MAX) // NSUInteger may be bigger than size_t (read param 3)
+ {
+ bytesToRead = SIZE_MAX;
+ }
+
+ // Make sure we have enough room in the buffer for our read.
+ //
+ // We are either reading directly into the currentRead->buffer,
+ // or we're reading into the temporary preBuffer.
+
+ uint8_t *buffer;
+
+ if (readIntoPreBuffer)
+ {
+ [preBuffer ensureCapacityForWrite:bytesToRead];
+
+ buffer = [preBuffer writeBuffer];
+ }
+ else
+ {
+ [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
+
+ buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone;
+ }
+
+ // Read data into buffer
+
+ size_t bytesRead = 0;
+
+ if (flags & kSocketSecure)
+ {
+ if ([self usingCFStreamForTLS])
+ {
+ #if TARGET_OS_IPHONE
+
+ CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead);
+ LogVerbose(@"CFReadStreamRead(): result = %i", (int)result);
+
+ if (result < 0)
+ {
+ error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream);
+ }
+ else if (result == 0)
+ {
+ socketEOF = YES;
+ }
+ else
+ {
+ waiting = YES;
+ bytesRead = (size_t)result;
+ }
+
+ // We only know how many decrypted bytes were read.
+ // The actual number of bytes read was likely more due to the overhead of the encryption.
+ // So we reset our flag, and rely on the next callback to alert us of more data.
+ flags &= ~kSecureSocketHasBytesAvailable;
+
+ #endif
+ }
+ else
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ // The documentation from Apple states:
+ //
+ // "a read operation might return errSSLWouldBlock,
+ // indicating that less data than requested was actually transferred"
+ //
+ // However, starting around 10.7, the function will sometimes return noErr,
+ // even if it didn't read as much data as requested. So we need to watch out for that.
+
+ OSStatus result;
+ do
+ {
+ void *loop_buffer = buffer + bytesRead;
+ size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead;
+ size_t loop_bytesRead = 0;
+
+ result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead);
+ LogVerbose(@"read from secure socket = %u", (unsigned)bytesRead);
+
+ bytesRead += loop_bytesRead;
+
+ } while ((result == noErr) && (bytesRead < bytesToRead));
+
+
+ if (result != noErr)
+ {
+ if (result == errSSLWouldBlock)
+ waiting = YES;
+ else
+ {
+ if (result == errSSLClosedGraceful || result == errSSLClosedAbort)
+ {
+ // We've reached the end of the stream.
+ // Handle this the same way we would an EOF from the socket.
+ socketEOF = YES;
+ sslErrCode = result;
+ }
+ else
+ {
+ error = [self sslError:result];
+ }
+ }
+ // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock.
+ // This happens when the SSLRead function is able to read some data,
+ // but not the entire amount we requested.
+
+ if (bytesRead <= 0)
+ {
+ bytesRead = 0;
+ }
+ }
+
+ // Do not modify socketFDBytesAvailable.
+ // It will be updated via the SSLReadFunction().
+
+ #endif
+ }
+ }
+ else
+ {
+ int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
+
+ ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
+ LogVerbose(@"read from socket = %i", (int)result);
+
+ if (result < 0)
+ {
+ if (errno == EWOULDBLOCK)
+ waiting = YES;
+ else
+ error = [self errnoErrorWithReason:@"Error in read() function"];
+
+ socketFDBytesAvailable = 0;
+ }
+ else if (result == 0)
+ {
+ socketEOF = YES;
+ socketFDBytesAvailable = 0;
+ }
+ else
+ {
+ bytesRead = result;
+
+ if (bytesRead < bytesToRead)
+ {
+ // The read returned less data than requested.
+ // This means socketFDBytesAvailable was a bit off due to timing,
+ // because we read from the socket right when the readSource event was firing.
+ socketFDBytesAvailable = 0;
+ }
+ else
+ {
+ if (socketFDBytesAvailable <= bytesRead)
+ socketFDBytesAvailable = 0;
+ else
+ socketFDBytesAvailable -= bytesRead;
+ }
+
+ if (socketFDBytesAvailable == 0)
+ {
+ waiting = YES;
+ }
+ }
+ }
+
+ if (bytesRead > 0)
+ {
+ // Check to see if the read operation is done
+
+ if (currentRead->readLength > 0)
+ {
+ // Read type #2 - read a specific length of data
+ //
+ // Note: We should never be using a prebuffer when we're reading a specific length of data.
+
+ NSAssert(readIntoPreBuffer == NO, @"Invalid logic");
+
+ currentRead->bytesDone += bytesRead;
+ totalBytesReadForCurrentRead += bytesRead;
+
+ done = (currentRead->bytesDone == currentRead->readLength);
+ }
+ else if (currentRead->term != nil)
+ {
+ // Read type #3 - read up to a terminator
+
+ if (readIntoPreBuffer)
+ {
+ // We just read a big chunk of data into the preBuffer
+
+ [preBuffer didWrite:bytesRead];
+ LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]);
+
+ // Search for the terminating sequence
+
+ bytesToRead = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
+ LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToRead);
+
+ // Ensure there's room on the read packet's buffer
+
+ [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];
+
+ // Copy bytes from prebuffer into read buffer
+
+ uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ + currentRead->bytesDone;
+
+ memcpy(readBuf, [preBuffer readBuffer], bytesToRead);
+
+ // Remove the copied bytes from the prebuffer
+ [preBuffer didRead:bytesToRead];
+ LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]);
+
+ // Update totals
+ currentRead->bytesDone += bytesToRead;
+ totalBytesReadForCurrentRead += bytesToRead;
+
+ // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above
+ }
+ else
+ {
+ // We just read a big chunk of data directly into the packet's buffer.
+ // We need to move any overflow into the prebuffer.
+
+ NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead];
+
+ if (overflow == 0)
+ {
+ // Perfect match!
+ // Every byte we read stays in the read buffer,
+ // and the last byte we read was the last byte of the term.
+
+ currentRead->bytesDone += bytesRead;
+ totalBytesReadForCurrentRead += bytesRead;
+ done = YES;
+ }
+ else if (overflow > 0)
+ {
+ // The term was found within the data that we read,
+ // and there are extra bytes that extend past the end of the term.
+ // We need to move these excess bytes out of the read packet and into the prebuffer.
+
+ NSInteger underflow = bytesRead - overflow;
+
+ // Copy excess data into preBuffer
+
+ LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow);
+ [preBuffer ensureCapacityForWrite:overflow];
+
+ uint8_t *overflowBuffer = buffer + underflow;
+ memcpy([preBuffer writeBuffer], overflowBuffer, overflow);
+
+ [preBuffer didWrite:overflow];
+ LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]);
+
+ // Note: The completeCurrentRead method will trim the buffer for us.
+
+ currentRead->bytesDone += underflow;
+ totalBytesReadForCurrentRead += underflow;
+ done = YES;
+ }
+ else
+ {
+ // The term was not found within the data that we read.
+
+ currentRead->bytesDone += bytesRead;
+ totalBytesReadForCurrentRead += bytesRead;
+ done = NO;
+ }
+ }
+
+ if (!done && currentRead->maxLength > 0)
+ {
+ // We're not done and there's a set maxLength.
+ // Have we reached that maxLength yet?
+
+ if (currentRead->bytesDone >= currentRead->maxLength)
+ {
+ error = [self readMaxedOutError];
+ }
+ }
+ }
+ else
+ {
+ // Read type #1 - read all available data
+
+ if (readIntoPreBuffer)
+ {
+ // We just read a chunk of data into the preBuffer
+
+ [preBuffer didWrite:bytesRead];
+
+ // Now copy the data into the read packet.
+ //
+ // Recall that we didn't read directly into the packet's buffer to avoid
+ // over-allocating memory since we had no clue how much data was available to be read.
+ //
+ // Ensure there's room on the read packet's buffer
+
+ [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead];
+
+ // Copy bytes from prebuffer into read buffer
+
+ uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset
+ + currentRead->bytesDone;
+
+ memcpy(readBuf, [preBuffer readBuffer], bytesRead);
+
+ // Remove the copied bytes from the prebuffer
+ [preBuffer didRead:bytesRead];
+
+ // Update totals
+ currentRead->bytesDone += bytesRead;
+ totalBytesReadForCurrentRead += bytesRead;
+ }
+ else
+ {
+ currentRead->bytesDone += bytesRead;
+ totalBytesReadForCurrentRead += bytesRead;
+ }
+
+ done = YES;
+ }
+
+ } // if (bytesRead > 0)
+
+ } // if (!done && !error && !socketEOF && !waiting && hasBytesAvailable)
+
+
+ if (!done && currentRead->readLength == 0 && currentRead->term == nil)
+ {
+ // Read type #1 - read all available data
+ //
+ // We might arrive here if we read data from the prebuffer but not from the socket.
+
+ done = (totalBytesReadForCurrentRead > 0);
+ }
+
+ // Check to see if we're done, or if we've made progress
+
+ if (done)
+ {
+ [self completeCurrentRead];
+
+ if (!error && (!socketEOF || [preBuffer availableBytes] > 0))
+ {
+ [self maybeDequeueRead];
+ }
+ }
+ else if (totalBytesReadForCurrentRead > 0)
+ {
+ // We're not done read type #2 or #3 yet, but we have read in some bytes
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)])
+ {
+ __strong id theDelegate = delegate;
+ long theReadTag = currentRead->tag;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag];
+ }});
+ }
+ }
+
+ // Check for errors
+
+ if (error)
+ {
+ [self closeWithError:error];
+ }
+ else if (socketEOF)
+ {
+ [self doReadEOF];
+ }
+ else if (waiting)
+ {
+ if (![self usingCFStreamForTLS])
+ {
+ // Monitor the socket for readability (if we're not already doing so)
+ [self resumeReadSource];
+ }
+ }
+
+ // Do not add any code here without first adding return statements in the error cases above.
+}
+
+- (void)doReadEOF
+{
+ LogTrace();
+
+ // This method may be called more than once.
+ // If the EOF is read while there is still data in the preBuffer,
+ // then this method may be called continually after invocations of doReadData to see if it's time to disconnect.
+
+ flags |= kSocketHasReadEOF;
+
+ if (flags & kSocketSecure)
+ {
+ // If the SSL layer has any buffered data, flush it into the preBuffer now.
+
+ [self flushSSLBuffers];
+ }
+
+ BOOL shouldDisconnect;
+ NSError *error = nil;
+
+ if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS))
+ {
+ // We received an EOF during or prior to startTLS.
+ // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation.
+
+ shouldDisconnect = YES;
+
+ if ([self usingSecureTransportForTLS])
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+ error = [self sslError:errSSLClosedAbort];
+ #endif
+ }
+ }
+ else if (flags & kReadStreamClosed)
+ {
+ // The preBuffer has already been drained.
+ // The config allows half-duplex connections.
+ // We've previously checked the socket, and it appeared writeable.
+ // So we marked the read stream as closed and notified the delegate.
+ //
+ // As per the half-duplex contract, the socket will be closed when a write fails,
+ // or when the socket is manually closed.
+
+ shouldDisconnect = NO;
+ }
+ else if ([preBuffer availableBytes] > 0)
+ {
+ LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer");
+
+ // Although we won't be able to read any more data from the socket,
+ // there is existing data that has been prebuffered that we can read.
+
+ shouldDisconnect = NO;
+ }
+ else if (config & kAllowHalfDuplexConnection)
+ {
+ // We just received an EOF (end of file) from the socket's read stream.
+ // This means the remote end of the socket (the peer we're connected to)
+ // has explicitly stated that it will not be sending us any more data.
+ //
+ // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us)
+
+ int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
+
+ struct pollfd pfd[1];
+ pfd[0].fd = socketFD;
+ pfd[0].events = POLLOUT;
+ pfd[0].revents = 0;
+
+ poll(pfd, 1, 0);
+
+ if (pfd[0].revents & POLLOUT)
+ {
+ // Socket appears to still be writeable
+
+ shouldDisconnect = NO;
+ flags |= kReadStreamClosed;
+
+ // Notify the delegate that we're going half-duplex
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socketDidCloseReadStream:)])
+ {
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socketDidCloseReadStream:self];
+ }});
+ }
+ }
+ else
+ {
+ shouldDisconnect = YES;
+ }
+ }
+ else
+ {
+ shouldDisconnect = YES;
+ }
+
+
+ if (shouldDisconnect)
+ {
+ if (error == nil)
+ {
+ if ([self usingSecureTransportForTLS])
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+ if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful)
+ {
+ error = [self sslError:sslErrCode];
+ }
+ else
+ {
+ error = [self connectionClosedError];
+ }
+ #endif
+ }
+ else
+ {
+ error = [self connectionClosedError];
+ }
+ }
+ [self closeWithError:error];
+ }
+ else
+ {
+ if (![self usingCFStreamForTLS])
+ {
+ // Suspend the read source (if needed)
+
+ [self suspendReadSource];
+ }
+ }
+}
+
+- (void)completeCurrentRead
+{
+ LogTrace();
+
+ NSAssert(currentRead, @"Trying to complete current read when there is no current read.");
+
+
+ NSData *result;
+
+ if (currentRead->bufferOwner)
+ {
+ // We created the buffer on behalf of the user.
+ // Trim our buffer to be the proper size.
+ [currentRead->buffer setLength:currentRead->bytesDone];
+
+ result = currentRead->buffer;
+ }
+ else
+ {
+ // We did NOT create the buffer.
+ // The buffer is owned by the caller.
+ // Only trim the buffer if we had to increase its size.
+
+ if ([currentRead->buffer length] > currentRead->originalBufferLength)
+ {
+ NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone;
+ NSUInteger origSize = currentRead->originalBufferLength;
+
+ NSUInteger buffSize = MAX(readSize, origSize);
+
+ [currentRead->buffer setLength:buffSize];
+ }
+
+ uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset;
+
+ result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO];
+ }
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadData:withTag:)])
+ {
+ __strong id theDelegate = delegate;
+ GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socket:self didReadData:result withTag:theRead->tag];
+ }});
+ }
+
+ [self endCurrentRead];
+}
+
+- (void)endCurrentRead
+{
+ if (readTimer)
+ {
+ dispatch_source_cancel(readTimer);
+ readTimer = NULL;
+ }
+
+ currentRead = nil;
+}
+
+- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout
+{
+ if (timeout >= 0.0)
+ {
+ readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
+
+ dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool {
+
+ [self doReadTimeout];
+ }});
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theReadTimer = readTimer;
+ dispatch_source_set_cancel_handler(readTimer, ^{
+ LogVerbose(@"dispatch_release(readTimer)");
+ dispatch_release(theReadTimer);
+ });
+ #endif
+
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
+
+ dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
+ dispatch_resume(readTimer);
+ }
+}
+
+- (void)doReadTimeout
+{
+ // This is a little bit tricky.
+ // Ideally we'd like to synchronously query the delegate about a timeout extension.
+ // But if we do so synchronously we risk a possible deadlock.
+ // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.
+
+ flags |= kReadsPaused;
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)])
+ {
+ __strong id theDelegate = delegate;
+ GCDAsyncReadPacket *theRead = currentRead;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ NSTimeInterval timeoutExtension = 0.0;
+
+ timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag
+ elapsed:theRead->timeout
+ bytesDone:theRead->bytesDone];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self doReadTimeoutWithExtension:timeoutExtension];
+ }});
+ }});
+ }
+ else
+ {
+ [self doReadTimeoutWithExtension:0.0];
+ }
+}
+
+- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension
+{
+ if (currentRead)
+ {
+ if (timeoutExtension > 0.0)
+ {
+ currentRead->timeout += timeoutExtension;
+
+ // Reschedule the timer
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC));
+ dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
+
+ // Unpause reads, and continue
+ flags &= ~kReadsPaused;
+ [self doReadData];
+ }
+ else
+ {
+ LogVerbose(@"ReadTimeout");
+
+ [self closeWithError:[self readTimeoutError]];
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Writing
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ if ([data length] == 0) return;
+
+ GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ LogTrace();
+
+ if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
+ {
+ [writeQueue addObject:packet];
+ [self maybeDequeueWrite];
+ }
+ }});
+
+ // Do not rely on the block being run in order to release the packet,
+ // as the queue might get released without the block completing.
+}
+
+- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr
+{
+ __block float result = 0.0F;
+
+ dispatch_block_t block = ^{
+
+ if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]])
+ {
+ // We're not writing anything right now.
+
+ if (tagPtr != NULL) *tagPtr = 0;
+ if (donePtr != NULL) *donePtr = 0;
+ if (totalPtr != NULL) *totalPtr = 0;
+
+ result = NAN;
+ }
+ else
+ {
+ NSUInteger done = currentWrite->bytesDone;
+ NSUInteger total = [currentWrite->buffer length];
+
+ if (tagPtr != NULL) *tagPtr = currentWrite->tag;
+ if (donePtr != NULL) *donePtr = done;
+ if (totalPtr != NULL) *totalPtr = total;
+
+ result = (float)done / (float)total;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+/**
+ * Conditionally starts a new write.
+ *
+ * It is called when:
+ * - a user requests a write
+ * - after a write request has finished (to handle the next request)
+ * - immediately after the socket opens to handle any pending requests
+ *
+ * This method also handles auto-disconnect post read/write completion.
+**/
+- (void)maybeDequeueWrite
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ // If we're not currently processing a write AND we have an available write stream
+ if ((currentWrite == nil) && (flags & kConnected))
+ {
+ if ([writeQueue count] > 0)
+ {
+ // Dequeue the next object in the write queue
+ currentWrite = [writeQueue objectAtIndex:0];
+ [writeQueue removeObjectAtIndex:0];
+
+
+ if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]])
+ {
+ LogVerbose(@"Dequeued GCDAsyncSpecialPacket");
+
+ // Attempt to start TLS
+ flags |= kStartingWriteTLS;
+
+ // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
+ [self maybeStartTLS];
+ }
+ else
+ {
+ LogVerbose(@"Dequeued GCDAsyncWritePacket");
+
+ // Setup write timer (if needed)
+ [self setupWriteTimerWithTimeout:currentWrite->timeout];
+
+ // Immediately write, if possible
+ [self doWriteData];
+ }
+ }
+ else if (flags & kDisconnectAfterWrites)
+ {
+ if (flags & kDisconnectAfterReads)
+ {
+ if (([readQueue count] == 0) && (currentRead == nil))
+ {
+ [self closeWithError:nil];
+ }
+ }
+ else
+ {
+ [self closeWithError:nil];
+ }
+ }
+ }
+}
+
+- (void)doWriteData
+{
+ LogTrace();
+
+ // This method is called by the writeSource via the socketQueue
+
+ if ((currentWrite == nil) || (flags & kWritesPaused))
+ {
+ LogVerbose(@"No currentWrite or kWritesPaused");
+
+ // Unable to write at this time
+
+ if ([self usingCFStreamForTLS])
+ {
+ // CFWriteStream only fires once when there is available data.
+ // It won't fire again until we've invoked CFWriteStreamWrite.
+ }
+ else
+ {
+ // If the writeSource is firing, we need to pause it
+ // or else it will continue to fire over and over again.
+
+ if (flags & kSocketCanAcceptBytes)
+ {
+ [self suspendWriteSource];
+ }
+ }
+ return;
+ }
+
+ if (!(flags & kSocketCanAcceptBytes))
+ {
+ LogVerbose(@"No space available to write...");
+
+ // No space available to write.
+
+ if (![self usingCFStreamForTLS])
+ {
+ // Need to wait for writeSource to fire and notify us of
+ // available space in the socket's internal write buffer.
+
+ [self resumeWriteSource];
+ }
+ return;
+ }
+
+ if (flags & kStartingWriteTLS)
+ {
+ LogVerbose(@"Waiting for SSL/TLS handshake to complete");
+
+ // The writeQueue is waiting for SSL/TLS handshake to complete.
+
+ if (flags & kStartingReadTLS)
+ {
+ if ([self usingSecureTransportForTLS])
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ // We are in the process of a SSL Handshake.
+ // We were waiting for available space in the socket's internal OS buffer to continue writing.
+
+ [self ssl_continueSSLHandshake];
+
+ #endif
+ }
+ }
+ else
+ {
+ // We are still waiting for the readQueue to drain and start the SSL/TLS process.
+ // We now know we can write to the socket.
+
+ if (![self usingCFStreamForTLS])
+ {
+ // Suspend the write source or else it will continue to fire nonstop.
+
+ [self suspendWriteSource];
+ }
+ }
+
+ return;
+ }
+
+ // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet)
+
+ BOOL waiting = NO;
+ NSError *error = nil;
+ size_t bytesWritten = 0;
+
+ if (flags & kSocketSecure)
+ {
+ if ([self usingCFStreamForTLS])
+ {
+ #if TARGET_OS_IPHONE
+
+ //
+ // Writing data using CFStream (over internal TLS)
+ //
+
+ const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;
+
+ NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;
+
+ if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
+ {
+ bytesToWrite = SIZE_MAX;
+ }
+
+ CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite);
+ LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result);
+
+ if (result < 0)
+ {
+ error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream);
+ }
+ else
+ {
+ bytesWritten = (size_t)result;
+
+ // We always set waiting to true in this scenario.
+ // CFStream may have altered our underlying socket to non-blocking.
+ // Thus if we attempt to write without a callback, we may end up blocking our queue.
+ waiting = YES;
+ }
+
+ #endif
+ }
+ else
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+ // We're going to use the SSLWrite function.
+ //
+ // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed)
+ //
+ // Parameters:
+ // context - An SSL session context reference.
+ // data - A pointer to the buffer of data to write.
+ // dataLength - The amount, in bytes, of data to write.
+ // processed - On return, the length, in bytes, of the data actually written.
+ //
+ // It sounds pretty straight-forward,
+ // but there are a few caveats you should be aware of.
+ //
+ // The SSLWrite method operates in a non-obvious (and rather annoying) manner.
+ // According to the documentation:
+ //
+ // Because you may configure the underlying connection to operate in a non-blocking manner,
+ // a write operation might return errSSLWouldBlock, indicating that less data than requested
+ // was actually transferred. In this case, you should repeat the call to SSLWrite until some
+ // other result is returned.
+ //
+ // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock,
+ // then the SSLWrite method returns (with the proper errSSLWouldBlock return value),
+ // but it sets processed to dataLength !!
+ //
+ // In other words, if the SSLWrite function doesn't completely write all the data we tell it to,
+ // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to
+ // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written.
+ //
+ // You might be wondering:
+ // If the SSLWrite function doesn't tell us how many bytes were written,
+ // then how in the world are we supposed to update our parameters (buffer & bytesToWrite)
+ // for the next time we invoke SSLWrite?
+ //
+ // The answer is that SSLWrite cached all the data we told it to write,
+ // and it will push out that data next time we call SSLWrite.
+ // If we call SSLWrite with new data, it will push out the cached data first, and then the new data.
+ // If we call SSLWrite with empty data, then it will simply push out the cached data.
+ //
+ // For this purpose we're going to break large writes into a series of smaller writes.
+ // This allows us to report progress back to the delegate.
+
+ OSStatus result;
+
+ BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0);
+ BOOL hasNewDataToWrite = YES;
+
+ if (hasCachedDataToWrite)
+ {
+ size_t processed = 0;
+
+ result = SSLWrite(sslContext, NULL, 0, &processed);
+
+ if (result == noErr)
+ {
+ bytesWritten = sslWriteCachedLength;
+ sslWriteCachedLength = 0;
+
+ if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten))
+ {
+ // We've written all data for the current write.
+ hasNewDataToWrite = NO;
+ }
+ }
+ else
+ {
+ if (result == errSSLWouldBlock)
+ {
+ waiting = YES;
+ }
+ else
+ {
+ error = [self sslError:result];
+ }
+
+ // Can't write any new data since we were unable to write the cached data.
+ hasNewDataToWrite = NO;
+ }
+ }
+
+ if (hasNewDataToWrite)
+ {
+ const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes]
+ + currentWrite->bytesDone
+ + bytesWritten;
+
+ NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten;
+
+ if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
+ {
+ bytesToWrite = SIZE_MAX;
+ }
+
+ size_t bytesRemaining = bytesToWrite;
+
+ BOOL keepLooping = YES;
+ while (keepLooping)
+ {
+ size_t sslBytesToWrite = MIN(bytesRemaining, 32768);
+ size_t sslBytesWritten = 0;
+
+ result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);
+
+ if (result == noErr)
+ {
+ buffer += sslBytesWritten;
+ bytesWritten += sslBytesWritten;
+ bytesRemaining -= sslBytesWritten;
+
+ keepLooping = (bytesRemaining > 0);
+ }
+ else
+ {
+ if (result == errSSLWouldBlock)
+ {
+ waiting = YES;
+ sslWriteCachedLength = sslBytesToWrite;
+ }
+ else
+ {
+ error = [self sslError:result];
+ }
+
+ keepLooping = NO;
+ }
+
+ } // while (keepLooping)
+
+ } // if (hasNewDataToWrite)
+
+ #endif
+ }
+ }
+ else
+ {
+ //
+ // Writing data directly over raw socket
+ //
+
+ int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
+
+ const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;
+
+ NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;
+
+ if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
+ {
+ bytesToWrite = SIZE_MAX;
+ }
+
+ ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
+ LogVerbose(@"wrote to socket = %zd", result);
+
+ // Check results
+ if (result < 0)
+ {
+ if (errno == EWOULDBLOCK)
+ {
+ waiting = YES;
+ }
+ else
+ {
+ error = [self errnoErrorWithReason:@"Error in write() function"];
+ }
+ }
+ else
+ {
+ bytesWritten = result;
+ }
+ }
+
+ // We're done with our writing.
+ // If we explictly ran into a situation where the socket told us there was no room in the buffer,
+ // then we immediately resume listening for notifications.
+ //
+ // We must do this before we dequeue another write,
+ // as that may in turn invoke this method again.
+ //
+ // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode.
+
+ if (waiting)
+ {
+ flags &= ~kSocketCanAcceptBytes;
+
+ if (![self usingCFStreamForTLS])
+ {
+ [self resumeWriteSource];
+ }
+ }
+
+ // Check our results
+
+ BOOL done = NO;
+
+ if (bytesWritten > 0)
+ {
+ // Update total amount read for the current write
+ currentWrite->bytesDone += bytesWritten;
+ LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone);
+
+ // Is packet done?
+ done = (currentWrite->bytesDone == [currentWrite->buffer length]);
+ }
+
+ if (done)
+ {
+ [self completeCurrentWrite];
+
+ if (!error)
+ {
+ [self maybeDequeueWrite];
+ }
+ }
+ else
+ {
+ // We were unable to finish writing the data,
+ // so we're waiting for another callback to notify us of available space in the lower-level output buffer.
+
+ if (!waiting & !error)
+ {
+ // This would be the case if our write was able to accept some data, but not all of it.
+
+ flags &= ~kSocketCanAcceptBytes;
+
+ if (![self usingCFStreamForTLS])
+ {
+ [self resumeWriteSource];
+ }
+ }
+
+ if (bytesWritten > 0)
+ {
+ // We're not done with the entire write, but we have written some bytes
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)])
+ {
+ __strong id theDelegate = delegate;
+ long theWriteTag = currentWrite->tag;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag];
+ }});
+ }
+ }
+ }
+
+ // Check for errors
+
+ if (error)
+ {
+ [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]];
+ }
+
+ // Do not add any code here without first adding a return statement in the error case above.
+}
+
+- (void)completeCurrentWrite
+{
+ LogTrace();
+
+ NSAssert(currentWrite, @"Trying to complete current write when there is no current write.");
+
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:didWriteDataWithTag:)])
+ {
+ __strong id theDelegate = delegate;
+ long theWriteTag = currentWrite->tag;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socket:self didWriteDataWithTag:theWriteTag];
+ }});
+ }
+
+ [self endCurrentWrite];
+}
+
+- (void)endCurrentWrite
+{
+ if (writeTimer)
+ {
+ dispatch_source_cancel(writeTimer);
+ writeTimer = NULL;
+ }
+
+ currentWrite = nil;
+}
+
+- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout
+{
+ if (timeout >= 0.0)
+ {
+ writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
+
+ dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool {
+
+ [self doWriteTimeout];
+ }});
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theWriteTimer = writeTimer;
+ dispatch_source_set_cancel_handler(writeTimer, ^{
+ LogVerbose(@"dispatch_release(writeTimer)");
+ dispatch_release(theWriteTimer);
+ });
+ #endif
+
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC));
+
+ dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0);
+ dispatch_resume(writeTimer);
+ }
+}
+
+- (void)doWriteTimeout
+{
+ // This is a little bit tricky.
+ // Ideally we'd like to synchronously query the delegate about a timeout extension.
+ // But if we do so synchronously we risk a possible deadlock.
+ // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.
+
+ flags |= kWritesPaused;
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)])
+ {
+ __strong id theDelegate = delegate;
+ GCDAsyncWritePacket *theWrite = currentWrite;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ NSTimeInterval timeoutExtension = 0.0;
+
+ timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag
+ elapsed:theWrite->timeout
+ bytesDone:theWrite->bytesDone];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self doWriteTimeoutWithExtension:timeoutExtension];
+ }});
+ }});
+ }
+ else
+ {
+ [self doWriteTimeoutWithExtension:0.0];
+ }
+}
+
+- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension
+{
+ if (currentWrite)
+ {
+ if (timeoutExtension > 0.0)
+ {
+ currentWrite->timeout += timeoutExtension;
+
+ // Reschedule the timer
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (timeoutExtension * NSEC_PER_SEC));
+ dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0);
+
+ // Unpause writes, and continue
+ flags &= ~kWritesPaused;
+ [self doWriteData];
+ }
+ else
+ {
+ LogVerbose(@"WriteTimeout");
+
+ [self closeWithError:[self writeTimeoutError]];
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Security
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)startTLS:(NSDictionary *)tlsSettings
+{
+ LogTrace();
+
+ if (tlsSettings == nil)
+ {
+ // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary,
+ // but causes problems if we later try to fetch the remote host's certificate.
+ //
+ // To be exact, it causes the following to return NULL instead of the normal result:
+ // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates)
+ //
+ // So we use an empty dictionary instead, which works perfectly.
+
+ tlsSettings = [NSDictionary dictionary];
+ }
+
+ GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
+ {
+ [readQueue addObject:packet];
+ [writeQueue addObject:packet];
+
+ flags |= kQueuedTLS;
+
+ [self maybeDequeueRead];
+ [self maybeDequeueWrite];
+ }
+ }});
+
+}
+
+- (void)maybeStartTLS
+{
+ // We can't start TLS until:
+ // - All queued reads prior to the user calling startTLS are complete
+ // - All queued writes prior to the user calling startTLS are complete
+ //
+ // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set
+
+ if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
+ {
+ BOOL canUseSecureTransport = YES;
+
+ #if TARGET_OS_IPHONE
+ {
+ GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
+ NSDictionary *tlsSettings = tlsPacket->tlsSettings;
+
+ NSNumber *value;
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot];
+ if (value && [value boolValue] == YES)
+ canUseSecureTransport = NO;
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots];
+ if (value && [value boolValue] == YES)
+ canUseSecureTransport = NO;
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain];
+ if (value && [value boolValue] == NO)
+ canUseSecureTransport = NO;
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates];
+ if (value && [value boolValue] == YES)
+ canUseSecureTransport = NO;
+ }
+ #endif
+
+ if (IS_SECURE_TRANSPORT_AVAILABLE && canUseSecureTransport)
+ {
+ #if SECURE_TRANSPORT_MAYBE_AVAILABLE
+ [self ssl_startTLS];
+ #endif
+ }
+ else
+ {
+ #if TARGET_OS_IPHONE
+ [self cf_startTLS];
+ #endif
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Security via SecureTransport
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength
+{
+ LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength);
+
+ if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0))
+ {
+ LogVerbose(@"%@ - No data available to read...", THIS_METHOD);
+
+ // No data available to read.
+ //
+ // Need to wait for readSource to fire and notify us of
+ // available data in the socket's internal read buffer.
+
+ [self resumeReadSource];
+
+ *bufferLength = 0;
+ return errSSLWouldBlock;
+ }
+
+ size_t totalBytesRead = 0;
+ size_t totalBytesLeftToBeRead = *bufferLength;
+
+ BOOL done = NO;
+ BOOL socketError = NO;
+
+ //
+ // STEP 1 : READ FROM SSL PRE BUFFER
+ //
+
+ size_t sslPreBufferLength = [sslPreBuffer availableBytes];
+
+ if (sslPreBufferLength > 0)
+ {
+ LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD);
+
+ size_t bytesToCopy;
+ if (sslPreBufferLength > totalBytesLeftToBeRead)
+ bytesToCopy = totalBytesLeftToBeRead;
+ else
+ bytesToCopy = sslPreBufferLength;
+
+ LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy);
+
+ memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy);
+ [sslPreBuffer didRead:bytesToCopy];
+
+ LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]);
+
+ totalBytesRead += bytesToCopy;
+ totalBytesLeftToBeRead -= bytesToCopy;
+
+ done = (totalBytesLeftToBeRead == 0);
+
+ if (done) LogVerbose(@"%@: Complete", THIS_METHOD);
+ }
+
+ //
+ // STEP 2 : READ FROM SOCKET
+ //
+
+ if (!done && (socketFDBytesAvailable > 0))
+ {
+ LogVerbose(@"%@: Reading from socket...", THIS_METHOD);
+
+ int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD;
+
+ BOOL readIntoPreBuffer;
+ size_t bytesToRead;
+ uint8_t *buf;
+
+ if (socketFDBytesAvailable > totalBytesLeftToBeRead)
+ {
+ // Read all available data from socket into sslPreBuffer.
+ // Then copy requested amount into dataBuffer.
+
+ LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD);
+
+ [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable];
+
+ readIntoPreBuffer = YES;
+ bytesToRead = (size_t)socketFDBytesAvailable;
+ buf = [sslPreBuffer writeBuffer];
+ }
+ else
+ {
+ // Read available data from socket directly into dataBuffer.
+
+ LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD);
+
+ readIntoPreBuffer = NO;
+ bytesToRead = totalBytesLeftToBeRead;
+ buf = (uint8_t *)buffer + totalBytesRead;
+ }
+
+ ssize_t result = read(socketFD, buf, bytesToRead);
+ LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result);
+
+ if (result < 0)
+ {
+ LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno);
+
+ if (errno != EWOULDBLOCK)
+ {
+ socketError = YES;
+ }
+
+ socketFDBytesAvailable = 0;
+ }
+ else if (result == 0)
+ {
+ LogVerbose(@"%@: read EOF", THIS_METHOD);
+
+ socketError = YES;
+ socketFDBytesAvailable = 0;
+ }
+ else
+ {
+ size_t bytesReadFromSocket = result;
+
+ if (socketFDBytesAvailable > bytesReadFromSocket)
+ socketFDBytesAvailable -= bytesReadFromSocket;
+ else
+ socketFDBytesAvailable = 0;
+
+ if (readIntoPreBuffer)
+ {
+ [sslPreBuffer didWrite:bytesReadFromSocket];
+
+ size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket);
+
+ LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy);
+
+ memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy);
+ [sslPreBuffer didRead:bytesToCopy];
+
+ totalBytesRead += bytesToCopy;
+ totalBytesLeftToBeRead -= bytesToCopy;
+
+ LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]);
+ }
+ else
+ {
+ totalBytesRead += bytesReadFromSocket;
+ totalBytesLeftToBeRead -= bytesReadFromSocket;
+ }
+
+ done = (totalBytesLeftToBeRead == 0);
+
+ if (done) LogVerbose(@"%@: Complete", THIS_METHOD);
+ }
+ }
+
+ *bufferLength = totalBytesRead;
+
+ if (done)
+ return noErr;
+
+ if (socketError)
+ return errSSLClosedAbort;
+
+ return errSSLWouldBlock;
+}
+
+- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength
+{
+ if (!(flags & kSocketCanAcceptBytes))
+ {
+ // Unable to write.
+ //
+ // Need to wait for writeSource to fire and notify us of
+ // available space in the socket's internal write buffer.
+
+ [self resumeWriteSource];
+
+ *bufferLength = 0;
+ return errSSLWouldBlock;
+ }
+
+ size_t bytesToWrite = *bufferLength;
+ size_t bytesWritten = 0;
+
+ BOOL done = NO;
+ BOOL socketError = NO;
+
+ int socketFD = (socket4FD == SOCKET_NULL) ? socket6FD : socket4FD;
+
+ ssize_t result = write(socketFD, buffer, bytesToWrite);
+
+ if (result < 0)
+ {
+ if (errno != EWOULDBLOCK)
+ {
+ socketError = YES;
+ }
+
+ flags &= ~kSocketCanAcceptBytes;
+ }
+ else if (result == 0)
+ {
+ flags &= ~kSocketCanAcceptBytes;
+ }
+ else
+ {
+ bytesWritten = result;
+
+ done = (bytesWritten == bytesToWrite);
+ }
+
+ *bufferLength = bytesWritten;
+
+ if (done)
+ return noErr;
+
+ if (socketError)
+ return errSSLClosedAbort;
+
+ return errSSLWouldBlock;
+}
+
+static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
+{
+ GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
+
+ NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
+
+ return [asyncSocket sslReadWithBuffer:data length:dataLength];
+}
+
+static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
+{
+ GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;
+
+ NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");
+
+ return [asyncSocket sslWriteWithBuffer:data length:dataLength];
+}
+
+- (void)ssl_startTLS
+{
+ LogTrace();
+
+ LogVerbose(@"Starting TLS (via SecureTransport)...");
+
+ OSStatus status;
+
+ GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
+ NSDictionary *tlsSettings = tlsPacket->tlsSettings;
+
+ // Create SSLContext, and setup IO callbacks and connection ref
+
+ BOOL isServer = [[tlsSettings objectForKey:(NSString *)kCFStreamSSLIsServer] boolValue];
+
+ #if TARGET_OS_IPHONE
+ {
+ if (isServer)
+ sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
+ else
+ sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
+
+ if (sslContext == NULL)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
+ return;
+ }
+ }
+ #else
+ {
+ status = SSLNewContext(isServer, &sslContext);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLNewContext"]];
+ return;
+ }
+ }
+ #endif
+
+ status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
+ return;
+ }
+
+ status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
+ return;
+ }
+
+ // Configure SSLContext from given settings
+ //
+ // Checklist:
+ // 1. kCFStreamSSLPeerName
+ // 2. kCFStreamSSLAllowsAnyRoot
+ // 3. kCFStreamSSLAllowsExpiredRoots
+ // 4. kCFStreamSSLValidatesCertificateChain
+ // 5. kCFStreamSSLAllowsExpiredCertificates
+ // 6. kCFStreamSSLCertificates
+ // 7. kCFStreamSSLLevel (GCDAsyncSocketSSLProtocolVersionMin / GCDAsyncSocketSSLProtocolVersionMax)
+ // 8. GCDAsyncSocketSSLCipherSuites
+ // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
+
+ id value;
+
+ // 1. kCFStreamSSLPeerName
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLPeerName];
+ if ([value isKindOfClass:[NSString class]])
+ {
+ NSString *peerName = (NSString *)value;
+
+ const char *peer = [peerName UTF8String];
+ size_t peerLen = strlen(peer);
+
+ status = SSLSetPeerDomainName(sslContext, peer, peerLen);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
+ return;
+ }
+ }
+
+ // 2. kCFStreamSSLAllowsAnyRoot
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsAnyRoot];
+ if (value)
+ {
+ #if TARGET_OS_IPHONE
+ NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsAnyRoot");
+ #else
+
+ BOOL allowsAnyRoot = [value boolValue];
+
+ status = SSLSetAllowsAnyRoot(sslContext, allowsAnyRoot);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetAllowsAnyRoot"]];
+ return;
+ }
+
+ #endif
+ }
+
+ // 3. kCFStreamSSLAllowsExpiredRoots
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredRoots];
+ if (value)
+ {
+ #if TARGET_OS_IPHONE
+ NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredRoots");
+ #else
+
+ BOOL allowsExpiredRoots = [value boolValue];
+
+ status = SSLSetAllowsExpiredRoots(sslContext, allowsExpiredRoots);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredRoots"]];
+ return;
+ }
+
+ #endif
+ }
+
+ // 4. kCFStreamSSLValidatesCertificateChain
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLValidatesCertificateChain];
+ if (value)
+ {
+ #if TARGET_OS_IPHONE
+ NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLValidatesCertificateChain");
+ #else
+
+ BOOL validatesCertChain = [value boolValue];
+
+ status = SSLSetEnableCertVerify(sslContext, validatesCertChain);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
+ return;
+ }
+
+ #endif
+ }
+
+ // 5. kCFStreamSSLAllowsExpiredCertificates
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLAllowsExpiredCertificates];
+ if (value)
+ {
+ #if TARGET_OS_IPHONE
+ NSAssert(NO, @"Security option unavailable via SecureTransport in iOS - kCFStreamSSLAllowsExpiredCertificates");
+ #else
+
+ BOOL allowsExpiredCerts = [value boolValue];
+
+ status = SSLSetAllowsExpiredCerts(sslContext, allowsExpiredCerts);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetAllowsExpiredCerts"]];
+ return;
+ }
+
+ #endif
+ }
+
+ // 6. kCFStreamSSLCertificates
+
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLCertificates];
+ if (value)
+ {
+ CFArrayRef certs = (__bridge CFArrayRef)value;
+
+ status = SSLSetCertificate(sslContext, certs);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]];
+ return;
+ }
+ }
+
+ // 7. kCFStreamSSLLevel
+
+ #if TARGET_OS_IPHONE
+ {
+ NSString *sslLevel = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel];
+
+ NSString *sslMinLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin];
+ NSString *sslMaxLevel = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax];
+
+ if (sslLevel)
+ {
+ if (sslMinLevel || sslMaxLevel)
+ {
+ LogWarn(@"kCFStreamSSLLevel security option ignored. Overriden by "
+ @"GCDAsyncSocketSSLProtocolVersionMin and/or GCDAsyncSocketSSLProtocolVersionMax");
+ }
+ else
+ {
+ if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv3])
+ {
+ sslMinLevel = sslMaxLevel = @"kSSLProtocol3";
+ }
+ else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1])
+ {
+ sslMinLevel = sslMaxLevel = @"kTLSProtocol1";
+ }
+ else
+ {
+ LogWarn(@"Unable to match kCFStreamSSLLevel security option to valid SSL protocol min/max");
+ }
+ }
+ }
+
+ if (sslMinLevel || sslMaxLevel)
+ {
+ OSStatus status1 = noErr;
+ OSStatus status2 = noErr;
+
+ SSLProtocol (^sslProtocolForString)(NSString*) = ^SSLProtocol (NSString *protocolStr) {
+
+ if ([protocolStr isEqualToString:@"kSSLProtocol3"]) return kSSLProtocol3;
+ if ([protocolStr isEqualToString:@"kTLSProtocol1"]) return kTLSProtocol1;
+ if ([protocolStr isEqualToString:@"kTLSProtocol11"]) return kTLSProtocol11;
+ if ([protocolStr isEqualToString:@"kTLSProtocol12"]) return kTLSProtocol12;
+
+ return kSSLProtocolUnknown;
+ };
+
+ SSLProtocol minProtocol = sslProtocolForString(sslMinLevel);
+ SSLProtocol maxProtocol = sslProtocolForString(sslMaxLevel);
+
+ if (minProtocol != kSSLProtocolUnknown)
+ {
+ status1 = SSLSetProtocolVersionMin(sslContext, minProtocol);
+ }
+ if (maxProtocol != kSSLProtocolUnknown)
+ {
+ status2 = SSLSetProtocolVersionMax(sslContext, maxProtocol);
+ }
+
+ if (status1 != noErr || status2 != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMinMax"]];
+ return;
+ }
+ }
+ }
+ #else
+ {
+ value = [tlsSettings objectForKey:(NSString *)kCFStreamSSLLevel];
+ if (value)
+ {
+ NSString *sslLevel = (NSString *)value;
+
+ OSStatus status1 = noErr;
+ OSStatus status2 = noErr;
+ OSStatus status3 = noErr;
+
+ if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv2])
+ {
+ // kCFStreamSocketSecurityLevelSSLv2:
+ //
+ // Specifies that SSL version 2 be set as the security protocol.
+
+ status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
+ status2 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES);
+ }
+ else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelSSLv3])
+ {
+ // kCFStreamSocketSecurityLevelSSLv3:
+ //
+ // Specifies that SSL version 3 be set as the security protocol.
+ // If SSL version 3 is not available, specifies that SSL version 2 be set as the security protocol.
+
+ status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
+ status2 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol2, YES);
+ status3 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocol3, YES);
+ }
+ else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelTLSv1])
+ {
+ // kCFStreamSocketSecurityLevelTLSv1:
+ //
+ // Specifies that TLS version 1 be set as the security protocol.
+
+ status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, NO);
+ status2 = SSLSetProtocolVersionEnabled(sslContext, kTLSProtocol1, YES);
+ }
+ else if ([sslLevel isEqualToString:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL])
+ {
+ // kCFStreamSocketSecurityLevelNegotiatedSSL:
+ //
+ // Specifies that the highest level security protocol that can be negotiated be used.
+
+ status1 = SSLSetProtocolVersionEnabled(sslContext, kSSLProtocolAll, YES);
+ }
+
+ if (status1 != noErr || status2 != noErr || status3 != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionEnabled"]];
+ return;
+ }
+ }
+ }
+ #endif
+
+ // 8. GCDAsyncSocketSSLCipherSuites
+
+ value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites];
+ if (value)
+ {
+ NSArray *cipherSuites = (NSArray *)value;
+ NSUInteger numberCiphers = [cipherSuites count];
+ SSLCipherSuite ciphers[numberCiphers];
+
+ NSUInteger cipherIndex;
+ for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++)
+ {
+ NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex];
+ ciphers[cipherIndex] = [cipherObject shortValue];
+ }
+
+ status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]];
+ return;
+ }
+ }
+
+ // 9. GCDAsyncSocketSSLDiffieHellmanParameters
+
+ #if !TARGET_OS_IPHONE
+ value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters];
+ if (value)
+ {
+ NSData *diffieHellmanData = (NSData *)value;
+
+ status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]);
+ if (status != noErr)
+ {
+ [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]];
+ return;
+ }
+ }
+ #endif
+
+ // Setup the sslPreBuffer
+ //
+ // Any data in the preBuffer needs to be moved into the sslPreBuffer,
+ // as this data is now part of the secure read stream.
+
+ sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
+
+ size_t preBufferLength = [preBuffer availableBytes];
+
+ if (preBufferLength > 0)
+ {
+ [sslPreBuffer ensureCapacityForWrite:preBufferLength];
+
+ memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
+ [preBuffer didRead:preBufferLength];
+ [sslPreBuffer didWrite:preBufferLength];
+ }
+
+ sslErrCode = noErr;
+
+ // Start the SSL Handshake process
+
+ [self ssl_continueSSLHandshake];
+}
+
+- (void)ssl_continueSSLHandshake
+{
+ LogTrace();
+
+ // If the return value is noErr, the session is ready for normal secure communication.
+ // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again.
+ // Otherwise, the return value indicates an error code.
+
+ OSStatus status = SSLHandshake(sslContext);
+
+ if (status == noErr)
+ {
+ LogVerbose(@"SSLHandshake complete");
+
+ flags &= ~kStartingReadTLS;
+ flags &= ~kStartingWriteTLS;
+
+ flags |= kSocketSecure;
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)])
+ {
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socketDidSecure:self];
+ }});
+ }
+
+ [self endCurrentRead];
+ [self endCurrentWrite];
+
+ [self maybeDequeueRead];
+ [self maybeDequeueWrite];
+ }
+ else if (status == errSSLWouldBlock)
+ {
+ LogVerbose(@"SSLHandshake continues...");
+
+ // Handshake continues...
+ //
+ // This method will be called again from doReadData or doWriteData.
+ }
+ else
+ {
+ [self closeWithError:[self sslError:status]];
+ }
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Security via CFStream
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if TARGET_OS_IPHONE
+
+- (void)cf_finishSSLHandshake
+{
+ LogTrace();
+
+ if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
+ {
+ flags &= ~kStartingReadTLS;
+ flags &= ~kStartingWriteTLS;
+
+ flags |= kSocketSecure;
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(socketDidSecure:)])
+ {
+ __strong id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate socketDidSecure:self];
+ }});
+ }
+
+ [self endCurrentRead];
+ [self endCurrentWrite];
+
+ [self maybeDequeueRead];
+ [self maybeDequeueWrite];
+ }
+}
+
+- (void)cf_abortSSLHandshake:(NSError *)error
+{
+ LogTrace();
+
+ if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
+ {
+ flags &= ~kStartingReadTLS;
+ flags &= ~kStartingWriteTLS;
+
+ [self closeWithError:error];
+ }
+}
+
+- (void)cf_startTLS
+{
+ LogTrace();
+
+ LogVerbose(@"Starting TLS (via CFStream)...");
+
+ if ([preBuffer availableBytes] > 0)
+ {
+ NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket.";
+
+ [self closeWithError:[self otherError:msg]];
+ return;
+ }
+
+ [self suspendReadSource];
+ [self suspendWriteSource];
+
+ socketFDBytesAvailable = 0;
+ flags &= ~kSocketCanAcceptBytes;
+ flags &= ~kSecureSocketHasBytesAvailable;
+
+ flags |= kUsingCFStreamForTLS;
+
+ if (![self createReadAndWriteStream])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
+ return;
+ }
+
+ if (![self registerForStreamCallbacksIncludingReadWrite:YES])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
+ return;
+ }
+
+ if (![self addStreamsToRunLoop])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
+ return;
+ }
+
+ NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS");
+ NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS");
+
+ GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
+ CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;
+
+ // Getting an error concerning kCFStreamPropertySSLSettings ?
+ // You need to add the CFNetwork framework to your iOS application.
+
+ BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
+ BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);
+
+ // For some reason, starting around the time of iOS 4.3,
+ // the first call to set the kCFStreamPropertySSLSettings will return true,
+ // but the second will return false.
+ //
+ // Order doesn't seem to matter.
+ // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order.
+ // Either way, the first call will return true, and the second returns false.
+ //
+ // Interestingly, this doesn't seem to affect anything.
+ // Which is not altogether unusual, as the documentation seems to suggest that (for many settings)
+ // setting it on one side of the stream automatically sets it for the other side of the stream.
+ //
+ // Although there isn't anything in the documentation to suggest that the second attempt would fail.
+ //
+ // Furthermore, this only seems to affect streams that are negotiating a security upgrade.
+ // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure
+ // connection, and then a startTLS is issued.
+ // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS).
+
+ if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
+ return;
+ }
+
+ if (![self openStreams])
+ {
+ [self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
+ return;
+ }
+
+ LogVerbose(@"Waiting for SSL Handshake to complete...");
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark CFStream
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if TARGET_OS_IPHONE
+
++ (void)startCFStreamThreadIfNeeded
+{
+ static dispatch_once_t predicate;
+ dispatch_once(&predicate, ^{
+
+ cfstreamThread = [[NSThread alloc] initWithTarget:self
+ selector:@selector(cfstreamThread)
+ object:nil];
+ [cfstreamThread start];
+ });
+}
+
++ (void)cfstreamThread { @autoreleasepool
+{
+ [[NSThread currentThread] setName:GCDAsyncSocketThreadName];
+
+ LogInfo(@"CFStreamThread: Started");
+
+ // We can't run the run loop unless it has an associated input source or a timer.
+ // So we'll just create a timer that will never fire - unless the server runs for decades.
+ [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
+ target:self
+ selector:@selector(doNothingAtAll:)
+ userInfo:nil
+ repeats:YES];
+
+ [[NSRunLoop currentRunLoop] run];
+
+ LogInfo(@"CFStreamThread: Stopped");
+}}
+
++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket
+{
+ LogTrace();
+ NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
+
+ CFRunLoopRef runLoop = CFRunLoopGetCurrent();
+
+ if (asyncSocket->readStream)
+ CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncSocket->writeStream)
+ CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
+}
+
++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket
+{
+ LogTrace();
+ NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
+
+ CFRunLoopRef runLoop = CFRunLoopGetCurrent();
+
+ if (asyncSocket->readStream)
+ CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncSocket->writeStream)
+ CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
+}
+
+static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
+{
+ GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo;
+
+ switch(type)
+ {
+ case kCFStreamEventHasBytesAvailable:
+ {
+ dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable");
+
+ if (asyncSocket->readStream != stream)
+ return_from_block;
+
+ if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
+ {
+ // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie.
+ // (A callback related to the tcp stream, but not to the SSL layer).
+
+ if (CFReadStreamHasBytesAvailable(asyncSocket->readStream))
+ {
+ asyncSocket->flags |= kSecureSocketHasBytesAvailable;
+ [asyncSocket cf_finishSSLHandshake];
+ }
+ }
+ else
+ {
+ asyncSocket->flags |= kSecureSocketHasBytesAvailable;
+ [asyncSocket doReadData];
+ }
+ }});
+
+ break;
+ }
+ default:
+ {
+ NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream);
+
+ if (error == nil && type == kCFStreamEventEndEncountered)
+ {
+ error = [asyncSocket connectionClosedError];
+ }
+
+ dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFReadStreamCallback - Other");
+
+ if (asyncSocket->readStream != stream)
+ return_from_block;
+
+ if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
+ {
+ [asyncSocket cf_abortSSLHandshake:error];
+ }
+ else
+ {
+ [asyncSocket closeWithError:error];
+ }
+ }});
+
+ break;
+ }
+ }
+
+}
+
+static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
+{
+ GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo;
+
+ switch(type)
+ {
+ case kCFStreamEventCanAcceptBytes:
+ {
+ dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes");
+
+ if (asyncSocket->writeStream != stream)
+ return_from_block;
+
+ if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
+ {
+ // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie.
+ // (A callback related to the tcp stream, but not to the SSL layer).
+
+ if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream))
+ {
+ asyncSocket->flags |= kSocketCanAcceptBytes;
+ [asyncSocket cf_finishSSLHandshake];
+ }
+ }
+ else
+ {
+ asyncSocket->flags |= kSocketCanAcceptBytes;
+ [asyncSocket doWriteData];
+ }
+ }});
+
+ break;
+ }
+ default:
+ {
+ NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream);
+
+ if (error == nil && type == kCFStreamEventEndEncountered)
+ {
+ error = [asyncSocket connectionClosedError];
+ }
+
+ dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFWriteStreamCallback - Other");
+
+ if (asyncSocket->writeStream != stream)
+ return_from_block;
+
+ if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS))
+ {
+ [asyncSocket cf_abortSSLHandshake:error];
+ }
+ else
+ {
+ [asyncSocket closeWithError:error];
+ }
+ }});
+
+ break;
+ }
+ }
+
+}
+
+- (BOOL)createReadAndWriteStream
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ if (readStream || writeStream)
+ {
+ // Streams already created
+ return YES;
+ }
+
+ int socketFD = (socket6FD == SOCKET_NULL) ? socket4FD : socket6FD;
+
+ if (socketFD == SOCKET_NULL)
+ {
+ // Cannot create streams without a file descriptor
+ return NO;
+ }
+
+ if (![self isConnected])
+ {
+ // Cannot create streams until file descriptor is connected
+ return NO;
+ }
+
+ LogVerbose(@"Creating read and write stream...");
+
+ CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream);
+
+ // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case).
+ // But let's not take any chances.
+
+ if (readStream)
+ CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+ if (writeStream)
+ CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+
+ if ((readStream == NULL) || (writeStream == NULL))
+ {
+ LogWarn(@"Unable to create read and write stream...");
+
+ if (readStream)
+ {
+ CFReadStreamClose(readStream);
+ CFRelease(readStream);
+ readStream = NULL;
+ }
+ if (writeStream)
+ {
+ CFWriteStreamClose(writeStream);
+ CFRelease(writeStream);
+ writeStream = NULL;
+ }
+
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite
+{
+ LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO"));
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
+
+ streamContext.version = 0;
+ streamContext.info = (__bridge void *)(self);
+ streamContext.retain = nil;
+ streamContext.release = nil;
+ streamContext.copyDescription = nil;
+
+ CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
+ if (includeReadWrite)
+ readStreamEvents |= kCFStreamEventHasBytesAvailable;
+
+ if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext))
+ {
+ return NO;
+ }
+
+ CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
+ if (includeReadWrite)
+ writeStreamEvents |= kCFStreamEventCanAcceptBytes;
+
+ if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext))
+ {
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)addStreamsToRunLoop
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
+
+ if (!(flags & kAddedStreamsToRunLoop))
+ {
+ LogVerbose(@"Adding streams to runloop...");
+
+ [[self class] startCFStreamThreadIfNeeded];
+ [[self class] performSelector:@selector(scheduleCFStreams:)
+ onThread:cfstreamThread
+ withObject:self
+ waitUntilDone:YES];
+
+ flags |= kAddedStreamsToRunLoop;
+ }
+
+ return YES;
+}
+
+- (void)removeStreamsFromRunLoop
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
+
+ if (flags & kAddedStreamsToRunLoop)
+ {
+ LogVerbose(@"Removing streams from runloop...");
+
+ [[self class] performSelector:@selector(unscheduleCFStreams:)
+ onThread:cfstreamThread
+ withObject:self
+ waitUntilDone:YES];
+
+ flags &= ~kAddedStreamsToRunLoop;
+ }
+}
+
+- (BOOL)openStreams
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null");
+
+ CFStreamStatus readStatus = CFReadStreamGetStatus(readStream);
+ CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream);
+
+ if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen))
+ {
+ LogVerbose(@"Opening read and write stream...");
+
+ BOOL r1 = CFReadStreamOpen(readStream);
+ BOOL r2 = CFWriteStreamOpen(writeStream);
+
+ if (!r1 || !r2)
+ {
+ LogError(@"Error in CFStreamOpen");
+ return NO;
+ }
+ }
+
+ return YES;
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Advanced
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * See header file for big discussion of this method.
+**/
+- (BOOL)autoDisconnectOnClosedReadStream
+{
+ // Note: YES means kAllowHalfDuplexConnection is OFF
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return ((config & kAllowHalfDuplexConnection) == 0);
+ }
+ else
+ {
+ __block BOOL result;
+
+ dispatch_sync(socketQueue, ^{
+ result = ((config & kAllowHalfDuplexConnection) == 0);
+ });
+
+ return result;
+ }
+}
+
+/**
+ * See header file for big discussion of this method.
+**/
+- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag
+{
+ // Note: YES means kAllowHalfDuplexConnection is OFF
+
+ dispatch_block_t block = ^{
+
+ if (flag)
+ config &= ~kAllowHalfDuplexConnection;
+ else
+ config |= kAllowHalfDuplexConnection;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+
+/**
+ * See header file for big discussion of this method.
+**/
+- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue
+{
+ void *nonNullUnusedPointer = (__bridge void *)self;
+ dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
+}
+
+/**
+ * See header file for big discussion of this method.
+**/
+- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue
+{
+ dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL);
+}
+
+/**
+ * See header file for big discussion of this method.
+**/
+- (void)performBlock:(dispatch_block_t)block
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+}
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (int)socketFD
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ if (socket4FD != SOCKET_NULL)
+ return socket4FD;
+ else
+ return socket6FD;
+}
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (int)socket4FD
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ return socket4FD;
+}
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (int)socket6FD
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ return socket6FD;
+}
+
+#if TARGET_OS_IPHONE
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (CFReadStreamRef)readStream
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return NULL;
+ }
+
+ if (readStream == NULL)
+ [self createReadAndWriteStream];
+
+ return readStream;
+}
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (CFWriteStreamRef)writeStream
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return NULL;
+ }
+
+ if (writeStream == NULL)
+ [self createReadAndWriteStream];
+
+ return writeStream;
+}
+
+- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat
+{
+ if (![self createReadAndWriteStream])
+ {
+ // Error occured creating streams (perhaps socket isn't open)
+ return NO;
+ }
+
+ BOOL r1, r2;
+
+ LogVerbose(@"Enabling backgrouding on socket");
+
+ r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+ r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+
+ if (!r1 || !r2)
+ {
+ return NO;
+ }
+
+ if (!caveat)
+ {
+ if (![self openStreams])
+ {
+ return NO;
+ }
+ }
+
+ return YES;
+}
+
+/**
+ * Questions? Have you read the header file?
+**/
+- (BOOL)enableBackgroundingOnSocket
+{
+ LogTrace();
+
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return NO;
+ }
+
+ return [self enableBackgroundingOnSocketWithCaveat:NO];
+}
+
+- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.???
+{
+ // This method was created as a workaround for a bug in iOS.
+ // Apple has since fixed this bug.
+ // I'm not entirely sure which version of iOS they fixed it in...
+
+ LogTrace();
+
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return NO;
+ }
+
+ return [self enableBackgroundingOnSocketWithCaveat:YES];
+}
+
+#endif
+
+#if SECURE_TRANSPORT_MAYBE_AVAILABLE
+
+- (SSLContextRef)sslContext
+{
+ if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD);
+ return NULL;
+ }
+
+ return sslContext;
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Class Methods
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
+{
+ char addrBuf[INET_ADDRSTRLEN];
+
+ if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
+ {
+ addrBuf[0] = '\0';
+ }
+
+ return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
+}
+
++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
+{
+ char addrBuf[INET6_ADDRSTRLEN];
+
+ if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
+ {
+ addrBuf[0] = '\0';
+ }
+
+ return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
+}
+
++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
+{
+ return ntohs(pSockaddr4->sin_port);
+}
+
++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
+{
+ return ntohs(pSockaddr6->sin6_port);
+}
+
++ (NSString *)hostFromAddress:(NSData *)address
+{
+ NSString *host;
+
+ if ([self getHost:&host port:NULL fromAddress:address])
+ return host;
+ else
+ return nil;
+}
+
++ (uint16_t)portFromAddress:(NSData *)address
+{
+ uint16_t port;
+
+ if ([self getHost:NULL port:&port fromAddress:address])
+ return port;
+ else
+ return 0;
+}
+
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address
+{
+ if ([address length] >= sizeof(struct sockaddr))
+ {
+ const struct sockaddr *sockaddrX = [address bytes];
+
+ if (sockaddrX->sa_family == AF_INET)
+ {
+ if ([address length] >= sizeof(struct sockaddr_in))
+ {
+ struct sockaddr_in sockaddr4;
+ memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4));
+
+ if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4];
+ if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4];
+
+ return YES;
+ }
+ }
+ else if (sockaddrX->sa_family == AF_INET6)
+ {
+ if ([address length] >= sizeof(struct sockaddr_in6))
+ {
+ struct sockaddr_in6 sockaddr6;
+ memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6));
+
+ if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6];
+ if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6];
+
+ return YES;
+ }
+ }
+ }
+
+ return NO;
+}
+
++ (NSData *)CRLFData
+{
+ return [NSData dataWithBytes:"\x0D\x0A" length:2];
+}
+
++ (NSData *)CRData
+{
+ return [NSData dataWithBytes:"\x0D" length:1];
+}
+
++ (NSData *)LFData
+{
+ return [NSData dataWithBytes:"\x0A" length:1];
+}
+
++ (NSData *)ZeroData
+{
+ return [NSData dataWithBytes:"" length:1];
+}
+
+@end
--- /dev/null
+//
+// GCDAsyncUdpSocket
+//
+// This class is in the public domain.
+// Originally created by Robbie Hanson of Deusty LLC.
+// Updated and maintained by Deusty LLC and the Apple development community.
+//
+// https://github.com/robbiehanson/CocoaAsyncSocket
+//
+
+#import <Foundation/Foundation.h>
+#import <dispatch/dispatch.h>
+
+
+extern NSString *const GCDAsyncUdpSocketException;
+extern NSString *const GCDAsyncUdpSocketErrorDomain;
+
+extern NSString *const GCDAsyncUdpSocketQueueName;
+extern NSString *const GCDAsyncUdpSocketThreadName;
+
+enum GCDAsyncUdpSocketError
+{
+ GCDAsyncUdpSocketNoError = 0, // Never used
+ GCDAsyncUdpSocketBadConfigError, // Invalid configuration
+ GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed
+ GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out
+ GCDAsyncUdpSocketClosedError, // The socket was closed
+ GCDAsyncUdpSocketOtherError, // Description provided in userInfo
+};
+typedef enum GCDAsyncUdpSocketError GCDAsyncUdpSocketError;
+
+/**
+ * You may optionally set a receive filter for the socket.
+ * A filter can provide several useful features:
+ *
+ * 1. Many times udp packets need to be parsed.
+ * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily.
+ * The end result is a parallel socket io, datagram parsing, and packet processing.
+ *
+ * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited.
+ * The filter can prevent such packets from arriving at the delegate.
+ * And because the filter can run in its own independent queue, this doesn't slow down the delegate.
+ *
+ * - Since the udp protocol does not guarantee delivery, udp packets may be lost.
+ * Many protocols built atop udp thus provide various resend/re-request algorithms.
+ * This sometimes results in duplicate packets arriving.
+ * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing.
+ *
+ * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive.
+ * Such packets need to be ignored.
+ *
+ * 3. Sometimes traffic shapers are needed to simulate real world environments.
+ * A filter allows you to write custom code to simulate such environments.
+ * The ability to code this yourself is especially helpful when your simulated environment
+ * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
+ * or the system tools to handle this aren't available (e.g. on a mobile device).
+ *
+ * @param data - The packet that was received.
+ * @param address - The address the data was received from.
+ * See utilities section for methods to extract info from address.
+ * @param context - Out parameter you may optionally set, which will then be passed to the delegate method.
+ * For example, filter block can parse the data and then,
+ * pass the parsed data to the delegate.
+ *
+ * @returns - YES if the received packet should be passed onto the delegate.
+ * NO if the received packet should be discarded, and not reported to the delegete.
+ *
+ * Example:
+ *
+ * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) {
+ *
+ * MyProtocolMessage *msg = [MyProtocol parseMessage:data];
+ *
+ * *context = response;
+ * return (response != nil);
+ * };
+ * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue];
+ *
+**/
+typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id *context);
+
+/**
+ * You may optionally set a send filter for the socket.
+ * A filter can provide several interesting possibilities:
+ *
+ * 1. Optional caching of resolved addresses for domain names.
+ * The cache could later be consulted, resulting in fewer system calls to getaddrinfo.
+ *
+ * 2. Reusable modules of code for bandwidth monitoring.
+ *
+ * 3. Sometimes traffic shapers are needed to simulate real world environments.
+ * A filter allows you to write custom code to simulate such environments.
+ * The ability to code this yourself is especially helpful when your simulated environment
+ * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
+ * or the system tools to handle this aren't available (e.g. on a mobile device).
+ *
+ * @param data - The packet that was received.
+ * @param address - The address the data was received from.
+ * See utilities section for methods to extract info from address.
+ * @param tag - The tag that was passed in the send method.
+ *
+ * @returns - YES if the packet should actually be sent over the socket.
+ * NO if the packet should be silently dropped (not sent over the socket).
+ *
+ * Regardless of the return value, the delegate will be informed that the packet was successfully sent.
+ *
+**/
+typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag);
+
+
+@interface GCDAsyncUdpSocket : NSObject
+
+/**
+ * GCDAsyncUdpSocket uses the standard delegate paradigm,
+ * but executes all delegate callbacks on a given delegate dispatch queue.
+ * This allows for maximum concurrency, while at the same time providing easy thread safety.
+ *
+ * You MUST set a delegate AND delegate dispatch queue before attempting to
+ * use the socket, or you will get an error.
+ *
+ * The socket queue is optional.
+ * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue.
+ * If you choose to provide a socket queue, the socket queue must not be a concurrent queue,
+ * then please see the discussion for the method markSocketQueueTargetQueue.
+ *
+ * The delegate queue and socket queue can optionally be the same.
+**/
+- (id)init;
+- (id)initWithSocketQueue:(dispatch_queue_t)sq;
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq;
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq;
+
+#pragma mark Configuration
+
+- (id)delegate;
+- (void)setDelegate:(id)delegate;
+- (void)synchronouslySetDelegate:(id)delegate;
+
+- (dispatch_queue_t)delegateQueue;
+- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue;
+- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue;
+
+- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr;
+- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
+- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
+
+/**
+ * By default, both IPv4 and IPv6 are enabled.
+ *
+ * This means GCDAsyncUdpSocket automatically supports both protocols,
+ * and can send to IPv4 or IPv6 addresses,
+ * as well as receive over IPv4 and IPv6.
+ *
+ * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6.
+ * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4.
+ * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6.
+ * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference.
+ * If IPv4 is preferred, then IPv4 is used.
+ * If IPv6 is preferred, then IPv6 is used.
+ * If neutral, then the first IP version in the resolved array will be used.
+ *
+ * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral.
+ * On prior systems the default IP preference is IPv4.
+ **/
+- (BOOL)isIPv4Enabled;
+- (void)setIPv4Enabled:(BOOL)flag;
+
+- (BOOL)isIPv6Enabled;
+- (void)setIPv6Enabled:(BOOL)flag;
+
+- (BOOL)isIPv4Preferred;
+- (BOOL)isIPv6Preferred;
+- (BOOL)isIPVersionNeutral;
+
+- (void)setPreferIPv4;
+- (void)setPreferIPv6;
+- (void)setIPVersionNeutral;
+
+/**
+ * Gets/Sets the maximum size of the buffer that will be allocated for receive operations.
+ * The default maximum size is 9216 bytes.
+ *
+ * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535.
+ * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295.
+ *
+ * Since the OS/GCD notifies us of the size of each received UDP packet,
+ * the actual allocated buffer size for each packet is exact.
+ * And in practice the size of UDP packets is generally much smaller than the max.
+ * Indeed most protocols will send and receive packets of only a few bytes,
+ * or will set a limit on the size of packets to prevent fragmentation in the IP layer.
+ *
+ * If you set the buffer size too small, the sockets API in the OS will silently discard
+ * any extra data, and you will not be notified of the error.
+**/
+- (uint16_t)maxReceiveIPv4BufferSize;
+- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max;
+
+- (uint32_t)maxReceiveIPv6BufferSize;
+- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max;
+
+/**
+ * User data allows you to associate arbitrary information with the socket.
+ * This data is not used internally in any way.
+**/
+- (id)userData;
+- (void)setUserData:(id)arbitraryUserData;
+
+#pragma mark Diagnostics
+
+/**
+ * Returns the local address info for the socket.
+ *
+ * The localAddress method returns a sockaddr structure wrapped in a NSData object.
+ * The localHost method returns the human readable IP address as a string.
+ *
+ * Note: Address info may not be available until after the socket has been binded, connected
+ * or until after data has been sent.
+**/
+- (NSData *)localAddress;
+- (NSString *)localHost;
+- (uint16_t)localPort;
+
+- (NSData *)localAddress_IPv4;
+- (NSString *)localHost_IPv4;
+- (uint16_t)localPort_IPv4;
+
+- (NSData *)localAddress_IPv6;
+- (NSString *)localHost_IPv6;
+- (uint16_t)localPort_IPv6;
+
+/**
+ * Returns the remote address info for the socket.
+ *
+ * The connectedAddress method returns a sockaddr structure wrapped in a NSData object.
+ * The connectedHost method returns the human readable IP address as a string.
+ *
+ * Note: Since UDP is connectionless by design, connected address info
+ * will not be available unless the socket is explicitly connected to a remote host/port.
+ * If the socket is not connected, these methods will return nil / 0.
+**/
+- (NSData *)connectedAddress;
+- (NSString *)connectedHost;
+- (uint16_t)connectedPort;
+
+/**
+ * Returns whether or not this socket has been connected to a single host.
+ * By design, UDP is a connectionless protocol, and connecting is not needed.
+ * If connected, the socket will only be able to send/receive data to/from the connected host.
+**/
+- (BOOL)isConnected;
+
+/**
+ * Returns whether or not this socket has been closed.
+ * The only way a socket can be closed is if you explicitly call one of the close methods.
+**/
+- (BOOL)isClosed;
+
+/**
+ * Returns whether or not this socket is IPv4.
+ *
+ * By default this will be true, unless:
+ * - IPv4 is disabled (via setIPv4Enabled:)
+ * - The socket is explicitly bound to an IPv6 address
+ * - The socket is connected to an IPv6 address
+**/
+- (BOOL)isIPv4;
+
+/**
+ * Returns whether or not this socket is IPv6.
+ *
+ * By default this will be true, unless:
+ * - IPv6 is disabled (via setIPv6Enabled:)
+ * - The socket is explicitly bound to an IPv4 address
+ * _ The socket is connected to an IPv4 address
+ *
+ * This method will also return false on platforms that do not support IPv6.
+ * Note: The iPhone does not currently support IPv6.
+**/
+- (BOOL)isIPv6;
+
+#pragma mark Binding
+
+/**
+ * Binds the UDP socket to the given port.
+ * Binding should be done for server sockets that receive data prior to sending it.
+ * Client sockets can skip binding,
+ * as the OS will automatically assign the socket an available port when it starts sending data.
+ *
+ * You may optionally pass a port number of zero to immediately bind the socket,
+ * yet still allow the OS to automatically assign an available port.
+ *
+ * You cannot bind a socket after its been connected.
+ * You can only bind a socket once.
+ * You can still connect a socket (if desired) after binding.
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
+**/
+- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr;
+
+/**
+ * Binds the UDP socket to the given port and optional interface.
+ * Binding should be done for server sockets that receive data prior to sending it.
+ * Client sockets can skip binding,
+ * as the OS will automatically assign the socket an available port when it starts sending data.
+ *
+ * You may optionally pass a port number of zero to immediately bind the socket,
+ * yet still allow the OS to automatically assign an available port.
+ *
+ * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
+ * You may also use the special strings "localhost" or "loopback" to specify that
+ * the socket only accept packets from the local machine.
+ *
+ * You cannot bind a socket after its been connected.
+ * You can only bind a socket once.
+ * You can still connect a socket (if desired) after binding.
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
+**/
+- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr;
+
+/**
+ * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object.
+ *
+ * If you have an existing struct sockaddr you can convert it to a NSData object like so:
+ * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
+ * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
+ *
+ * Binding should be done for server sockets that receive data prior to sending it.
+ * Client sockets can skip binding,
+ * as the OS will automatically assign the socket an available port when it starts sending data.
+ *
+ * You cannot bind a socket after its been connected.
+ * You can only bind a socket once.
+ * You can still connect a socket (if desired) after binding.
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr.
+**/
+- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr;
+
+#pragma mark Connecting
+
+/**
+ * Connects the UDP socket to the given host and port.
+ * By design, UDP is a connectionless protocol, and connecting is not needed.
+ *
+ * Choosing to connect to a specific host/port has the following effect:
+ * - You will only be able to send data to the connected host/port.
+ * - You will only be able to receive data from the connected host/port.
+ * - You will receive ICMP messages that come from the connected host/port, such as "connection refused".
+ *
+ * The actual process of connecting a UDP socket does not result in any communication on the socket.
+ * It simply changes the internal state of the socket.
+ *
+ * You cannot bind a socket after it has been connected.
+ * You can only connect a socket once.
+ *
+ * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2").
+ *
+ * This method is asynchronous as it requires a DNS lookup to resolve the given host name.
+ * If an obvious error is detected, this method immediately returns NO and sets errPtr.
+ * If you don't care about the error, you can pass nil for errPtr.
+ * Otherwise, this method returns YES and begins the asynchronous connection process.
+ * The result of the asynchronous connection process will be reported via the delegate methods.
+ **/
+- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
+
+/**
+ * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object.
+ *
+ * If you have an existing struct sockaddr you can convert it to a NSData object like so:
+ * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len];
+ * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len];
+ *
+ * By design, UDP is a connectionless protocol, and connecting is not needed.
+ *
+ * Choosing to connect to a specific address has the following effect:
+ * - You will only be able to send data to the connected address.
+ * - You will only be able to receive data from the connected address.
+ * - You will receive ICMP messages that come from the connected address, such as "connection refused".
+ *
+ * Connecting a UDP socket does not result in any communication on the socket.
+ * It simply changes the internal state of the socket.
+ *
+ * You cannot bind a socket after its been connected.
+ * You can only connect a socket once.
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
+ *
+ * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup.
+ * Thus when this method returns, the connection has either failed or fully completed.
+ * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method.
+ * However, for compatibility and simplification of delegate code, if this method returns YES
+ * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked.
+**/
+- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr;
+
+#pragma mark Multicast
+
+/**
+ * Join multicast group.
+ * Group should be an IP address (eg @"225.228.0.1").
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
+**/
+- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr;
+
+/**
+ * Join multicast group.
+ * Group should be an IP address (eg @"225.228.0.1").
+ * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35").
+ *
+ * On success, returns YES.
+ * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr.
+**/
+- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr;
+
+- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr;
+- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr;
+
+#pragma mark Broadcast
+
+/**
+ * By default, the underlying socket in the OS will not allow you to send broadcast messages.
+ * In order to send broadcast messages, you need to enable this functionality in the socket.
+ *
+ * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is
+ * delivered to every host on the network.
+ * The reason this is generally disabled by default (by the OS) is to prevent
+ * accidental broadcast messages from flooding the network.
+**/
+- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr;
+
+#pragma mark Sending
+
+/**
+ * Asynchronously sends the given data, with the given timeout and tag.
+ *
+ * This method may only be used with a connected socket.
+ * Recall that connecting is optional for a UDP socket.
+ * For connected sockets, data can only be sent to the connected address.
+ * For non-connected sockets, the remote destination is specified for each packet.
+ * For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
+ *
+ * @param data
+ * The data to send.
+ * If data is nil or zero-length, this method does nothing.
+ * If passing NSMutableData, please read the thread-safety notice below.
+ *
+ * @param timeout
+ * The timeout for the send opeartion.
+ * If the timeout value is negative, the send operation will not use a timeout.
+ *
+ * @param tag
+ * The tag is for your convenience.
+ * It is not sent or received over the socket in any manner what-so-ever.
+ * It is reported back as a parameter in the udpSocket:didSendDataWithTag:
+ * or udpSocket:didNotSendDataWithTag:dueToError: methods.
+ * You can use it as an array index, state id, type constant, etc.
+ *
+ *
+ * Thread-Safety Note:
+ * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
+ * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
+ * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
+ * that this particular send operation has completed.
+ * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
+ * It simply retains it for performance reasons.
+ * Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
+ * Copying this data adds an unwanted/unneeded overhead.
+ * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
+ * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
+ * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
+**/
+- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Asynchronously sends the given data, with the given timeout and tag, to the given host and port.
+ *
+ * This method cannot be used with a connected socket.
+ * Recall that connecting is optional for a UDP socket.
+ * For connected sockets, data can only be sent to the connected address.
+ * For non-connected sockets, the remote destination is specified for each packet.
+ * For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
+ *
+ * @param data
+ * The data to send.
+ * If data is nil or zero-length, this method does nothing.
+ * If passing NSMutableData, please read the thread-safety notice below.
+ *
+ * @param host
+ * The destination to send the udp packet to.
+ * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2").
+ * You may also use the convenience strings of "loopback" or "localhost".
+ *
+ * @param port
+ * The port of the host to send to.
+ *
+ * @param timeout
+ * The timeout for the send opeartion.
+ * If the timeout value is negative, the send operation will not use a timeout.
+ *
+ * @param tag
+ * The tag is for your convenience.
+ * It is not sent or received over the socket in any manner what-so-ever.
+ * It is reported back as a parameter in the udpSocket:didSendDataWithTag:
+ * or udpSocket:didNotSendDataWithTag:dueToError: methods.
+ * You can use it as an array index, state id, type constant, etc.
+ *
+ *
+ * Thread-Safety Note:
+ * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
+ * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
+ * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
+ * that this particular send operation has completed.
+ * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
+ * It simply retains it for performance reasons.
+ * Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
+ * Copying this data adds an unwanted/unneeded overhead.
+ * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
+ * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
+ * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
+**/
+- (void)sendData:(NSData *)data
+ toHost:(NSString *)host
+ port:(uint16_t)port
+ withTimeout:(NSTimeInterval)timeout
+ tag:(long)tag;
+
+/**
+ * Asynchronously sends the given data, with the given timeout and tag, to the given address.
+ *
+ * This method cannot be used with a connected socket.
+ * Recall that connecting is optional for a UDP socket.
+ * For connected sockets, data can only be sent to the connected address.
+ * For non-connected sockets, the remote destination is specified for each packet.
+ * For more information about optionally connecting udp sockets, see the documentation for the connect methods above.
+ *
+ * @param data
+ * The data to send.
+ * If data is nil or zero-length, this method does nothing.
+ * If passing NSMutableData, please read the thread-safety notice below.
+ *
+ * @param address
+ * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object).
+ *
+ * @param timeout
+ * The timeout for the send opeartion.
+ * If the timeout value is negative, the send operation will not use a timeout.
+ *
+ * @param tag
+ * The tag is for your convenience.
+ * It is not sent or received over the socket in any manner what-so-ever.
+ * It is reported back as a parameter in the udpSocket:didSendDataWithTag:
+ * or udpSocket:didNotSendDataWithTag:dueToError: methods.
+ * You can use it as an array index, state id, type constant, etc.
+ *
+ *
+ * Thread-Safety Note:
+ * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while
+ * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method
+ * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying
+ * that this particular send operation has completed.
+ * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data.
+ * It simply retains it for performance reasons.
+ * Often times, if NSMutableData is passed, it is because a request/response was built up in memory.
+ * Copying this data adds an unwanted/unneeded overhead.
+ * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket
+ * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time
+ * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method.
+**/
+- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * You may optionally set a send filter for the socket.
+ * A filter can provide several interesting possibilities:
+ *
+ * 1. Optional caching of resolved addresses for domain names.
+ * The cache could later be consulted, resulting in fewer system calls to getaddrinfo.
+ *
+ * 2. Reusable modules of code for bandwidth monitoring.
+ *
+ * 3. Sometimes traffic shapers are needed to simulate real world environments.
+ * A filter allows you to write custom code to simulate such environments.
+ * The ability to code this yourself is especially helpful when your simulated environment
+ * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
+ * or the system tools to handle this aren't available (e.g. on a mobile device).
+ *
+ * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef.
+ * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue.
+ *
+ * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below),
+ * passing YES for the isAsynchronous parameter.
+**/
+- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue;
+
+/**
+ * The receive filter can be run via dispatch_async or dispatch_sync.
+ * Most typical situations call for asynchronous operation.
+ *
+ * However, there are a few situations in which synchronous operation is preferred.
+ * Such is the case when the filter is extremely minimal and fast.
+ * This is because dispatch_sync is faster than dispatch_async.
+ *
+ * If you choose synchronous operation, be aware of possible deadlock conditions.
+ * Since the socket queue is executing your block via dispatch_sync,
+ * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue.
+ * For example, you can't query properties on the socket.
+**/
+- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock
+ withQueue:(dispatch_queue_t)filterQueue
+ isAsynchronous:(BOOL)isAsynchronous;
+
+#pragma mark Receiving
+
+/**
+ * There are two modes of operation for receiving packets: one-at-a-time & continuous.
+ *
+ * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet.
+ * Receiving packets one-at-a-time may be better suited for implementing certain state machine code,
+ * where your state machine may not always be ready to process incoming packets.
+ *
+ * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received.
+ * Receiving packets continuously is better suited to real-time streaming applications.
+ *
+ * You may switch back and forth between one-at-a-time mode and continuous mode.
+ * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode.
+ *
+ * When a packet is received (and not filtered by the optional receive filter),
+ * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked.
+ *
+ * If the socket is able to begin receiving packets, this method returns YES.
+ * Otherwise it returns NO, and sets the errPtr with appropriate error information.
+ *
+ * An example error:
+ * You created a udp socket to act as a server, and immediately called receive.
+ * You forgot to first bind the socket to a port number, and received a error with a message like:
+ * "Must bind socket before you can receive data."
+**/
+- (BOOL)receiveOnce:(NSError **)errPtr;
+
+/**
+ * There are two modes of operation for receiving packets: one-at-a-time & continuous.
+ *
+ * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet.
+ * Receiving packets one-at-a-time may be better suited for implementing certain state machine code,
+ * where your state machine may not always be ready to process incoming packets.
+ *
+ * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received.
+ * Receiving packets continuously is better suited to real-time streaming applications.
+ *
+ * You may switch back and forth between one-at-a-time mode and continuous mode.
+ * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode.
+ *
+ * For every received packet (not filtered by the optional receive filter),
+ * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked.
+ *
+ * If the socket is able to begin receiving packets, this method returns YES.
+ * Otherwise it returns NO, and sets the errPtr with appropriate error information.
+ *
+ * An example error:
+ * You created a udp socket to act as a server, and immediately called receive.
+ * You forgot to first bind the socket to a port number, and received a error with a message like:
+ * "Must bind socket before you can receive data."
+**/
+- (BOOL)beginReceiving:(NSError **)errPtr;
+
+/**
+ * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving.
+ * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again.
+ *
+ * Important Note:
+ * GCDAsyncUdpSocket may be running in parallel with your code.
+ * That is, your delegate is likely running on a separate thread/dispatch_queue.
+ * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked.
+ * Thus, if those delegate methods have already been dispatch_async'd,
+ * your didReceive delegate method may still be invoked after this method has been called.
+ * You should be aware of this, and program defensively.
+**/
+- (void)pauseReceiving;
+
+/**
+ * You may optionally set a receive filter for the socket.
+ * This receive filter may be set to run in its own queue (independent of delegate queue).
+ *
+ * A filter can provide several useful features.
+ *
+ * 1. Many times udp packets need to be parsed.
+ * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily.
+ * The end result is a parallel socket io, datagram parsing, and packet processing.
+ *
+ * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited.
+ * The filter can prevent such packets from arriving at the delegate.
+ * And because the filter can run in its own independent queue, this doesn't slow down the delegate.
+ *
+ * - Since the udp protocol does not guarantee delivery, udp packets may be lost.
+ * Many protocols built atop udp thus provide various resend/re-request algorithms.
+ * This sometimes results in duplicate packets arriving.
+ * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing.
+ *
+ * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive.
+ * Such packets need to be ignored.
+ *
+ * 3. Sometimes traffic shapers are needed to simulate real world environments.
+ * A filter allows you to write custom code to simulate such environments.
+ * The ability to code this yourself is especially helpful when your simulated environment
+ * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router),
+ * or the system tools to handle this aren't available (e.g. on a mobile device).
+ *
+ * Example:
+ *
+ * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) {
+ *
+ * MyProtocolMessage *msg = [MyProtocol parseMessage:data];
+ *
+ * *context = response;
+ * return (response != nil);
+ * };
+ * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue];
+ *
+ * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef.
+ * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue.
+ *
+ * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below),
+ * passing YES for the isAsynchronous parameter.
+**/
+- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue;
+
+/**
+ * The receive filter can be run via dispatch_async or dispatch_sync.
+ * Most typical situations call for asynchronous operation.
+ *
+ * However, there are a few situations in which synchronous operation is preferred.
+ * Such is the case when the filter is extremely minimal and fast.
+ * This is because dispatch_sync is faster than dispatch_async.
+ *
+ * If you choose synchronous operation, be aware of possible deadlock conditions.
+ * Since the socket queue is executing your block via dispatch_sync,
+ * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue.
+ * For example, you can't query properties on the socket.
+**/
+- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock
+ withQueue:(dispatch_queue_t)filterQueue
+ isAsynchronous:(BOOL)isAsynchronous;
+
+#pragma mark Closing
+
+/**
+ * Immediately closes the underlying socket.
+ * Any pending send operations are discarded.
+ *
+ * The GCDAsyncUdpSocket instance may optionally be used again.
+ * (it will setup/configure/use another unnderlying BSD socket).
+**/
+- (void)close;
+
+/**
+ * Closes the underlying socket after all pending send operations have been sent.
+ *
+ * The GCDAsyncUdpSocket instance may optionally be used again.
+ * (it will setup/configure/use another unnderlying BSD socket).
+**/
+- (void)closeAfterSending;
+
+#pragma mark Advanced
+/**
+ * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue.
+ * In most cases, the instance creates this queue itself.
+ * However, to allow for maximum flexibility, the internal queue may be passed in the init method.
+ * This allows for some advanced options such as controlling socket priority via target queues.
+ * However, when one begins to use target queues like this, they open the door to some specific deadlock issues.
+ *
+ * For example, imagine there are 2 queues:
+ * dispatch_queue_t socketQueue;
+ * dispatch_queue_t socketTargetQueue;
+ *
+ * If you do this (pseudo-code):
+ * socketQueue.targetQueue = socketTargetQueue;
+ *
+ * Then all socketQueue operations will actually get run on the given socketTargetQueue.
+ * This is fine and works great in most situations.
+ * But if you run code directly from within the socketTargetQueue that accesses the socket,
+ * you could potentially get deadlock. Imagine the following code:
+ *
+ * - (BOOL)socketHasSomething
+ * {
+ * __block BOOL result = NO;
+ * dispatch_block_t block = ^{
+ * result = [self someInternalMethodToBeRunOnlyOnSocketQueue];
+ * }
+ * if (is_executing_on_queue(socketQueue))
+ * block();
+ * else
+ * dispatch_sync(socketQueue, block);
+ *
+ * return result;
+ * }
+ *
+ * What happens if you call this method from the socketTargetQueue? The result is deadlock.
+ * This is because the GCD API offers no mechanism to discover a queue's targetQueue.
+ * Thus we have no idea if our socketQueue is configured with a targetQueue.
+ * If we had this information, we could easily avoid deadlock.
+ * But, since these API's are missing or unfeasible, you'll have to explicitly set it.
+ *
+ * IF you pass a socketQueue via the init method,
+ * AND you've configured the passed socketQueue with a targetQueue,
+ * THEN you should pass the end queue in the target hierarchy.
+ *
+ * For example, consider the following queue hierarchy:
+ * socketQueue -> ipQueue -> moduleQueue
+ *
+ * This example demonstrates priority shaping within some server.
+ * All incoming client connections from the same IP address are executed on the same target queue.
+ * And all connections for a particular module are executed on the same target queue.
+ * Thus, the priority of all networking for the entire module can be changed on the fly.
+ * Additionally, networking traffic from a single IP cannot monopolize the module.
+ *
+ * Here's how you would accomplish something like that:
+ * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock
+ * {
+ * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL);
+ * dispatch_queue_t ipQueue = [self ipQueueForAddress:address];
+ *
+ * dispatch_set_target_queue(socketQueue, ipQueue);
+ * dispatch_set_target_queue(iqQueue, moduleQueue);
+ *
+ * return socketQueue;
+ * }
+ * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
+ * {
+ * [clientConnections addObject:newSocket];
+ * [newSocket markSocketQueueTargetQueue:moduleQueue];
+ * }
+ *
+ * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue.
+ * This is often NOT the case, as such queues are used solely for execution shaping.
+ **/
+- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue;
+- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue;
+
+/**
+ * It's not thread-safe to access certain variables from outside the socket's internal queue.
+ *
+ * For example, the socket file descriptor.
+ * File descriptors are simply integers which reference an index in the per-process file table.
+ * However, when one requests a new file descriptor (by opening a file or socket),
+ * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor.
+ * So if we're not careful, the following could be possible:
+ *
+ * - Thread A invokes a method which returns the socket's file descriptor.
+ * - The socket is closed via the socket's internal queue on thread B.
+ * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD.
+ * - Thread A is now accessing/altering the file instead of the socket.
+ *
+ * In addition to this, other variables are not actually objects,
+ * and thus cannot be retained/released or even autoreleased.
+ * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct.
+ *
+ * Although there are internal variables that make it difficult to maintain thread-safety,
+ * it is important to provide access to these variables
+ * to ensure this class can be used in a wide array of environments.
+ * This method helps to accomplish this by invoking the current block on the socket's internal queue.
+ * The methods below can be invoked from within the block to access
+ * those generally thread-unsafe internal variables in a thread-safe manner.
+ * The given block will be invoked synchronously on the socket's internal queue.
+ *
+ * If you save references to any protected variables and use them outside the block, you do so at your own peril.
+**/
+- (void)performBlock:(dispatch_block_t)block;
+
+/**
+ * These methods are only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Provides access to the socket's file descriptor(s).
+ * If the socket isn't connected, or explicity bound to a particular interface,
+ * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6.
+**/
+- (int)socketFD;
+- (int)socket4FD;
+- (int)socket6FD;
+
+#if TARGET_OS_IPHONE
+
+/**
+ * These methods are only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket.
+ *
+ * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.)
+ * However, if you need one for any reason,
+ * these methods are a convenient way to get access to a safe instance of one.
+**/
+- (CFReadStreamRef)readStream;
+- (CFWriteStreamRef)writeStream;
+
+/**
+ * This method is only available from within the context of a performBlock: invocation.
+ * See the documentation for the performBlock: method above.
+ *
+ * Configures the socket to allow it to operate when the iOS application has been backgrounded.
+ * In other words, this method creates a read & write stream, and invokes:
+ *
+ * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+ * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+ *
+ * Returns YES if successful, NO otherwise.
+ *
+ * Example usage:
+ *
+ * [asyncUdpSocket performBlock:^{
+ * [asyncUdpSocket enableBackgroundingOnSocket];
+ * }];
+ *
+ *
+ * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now).
+**/
+//- (BOOL)enableBackgroundingOnSockets;
+
+#endif
+
+#pragma mark Utilities
+
+/**
+ * Extracting host/port/family information from raw address data.
+**/
+
++ (NSString *)hostFromAddress:(NSData *)address;
++ (uint16_t)portFromAddress:(NSData *)address;
++ (int)familyFromAddress:(NSData *)address;
+
++ (BOOL)isIPv4Address:(NSData *)address;
++ (BOOL)isIPv6Address:(NSData *)address;
+
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address;
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@protocol GCDAsyncUdpSocketDelegate
+@optional
+
+/**
+ * By design, UDP is a connectionless protocol, and connecting is not needed.
+ * However, you may optionally choose to connect to a particular host for reasons
+ * outlined in the documentation for the various connect methods listed above.
+ *
+ * This method is called if one of the connect methods are invoked, and the connection is successful.
+**/
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address;
+
+/**
+ * By design, UDP is a connectionless protocol, and connecting is not needed.
+ * However, you may optionally choose to connect to a particular host for reasons
+ * outlined in the documentation for the various connect methods listed above.
+ *
+ * This method is called if one of the connect methods are invoked, and the connection fails.
+ * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved.
+**/
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error;
+
+/**
+ * Called when the datagram with the given tag has been sent.
+**/
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag;
+
+/**
+ * Called if an error occurs while trying to send a datagram.
+ * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet.
+**/
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error;
+
+/**
+ * Called when the socket has received the requested datagram.
+**/
+- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data
+ fromAddress:(NSData *)address
+ withFilterContext:(id)filterContext;
+
+/**
+ * Called when the socket is closed.
+**/
+- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError *)error;
+
+@end
+
--- /dev/null
+//
+// GCDAsyncUdpSocket
+//
+// This class is in the public domain.
+// Originally created by Robbie Hanson of Deusty LLC.
+// Updated and maintained by Deusty LLC and the Apple development community.
+//
+// https://github.com/robbiehanson/CocoaAsyncSocket
+//
+
+#import "GCDAsyncUdpSocket.h"
+
+#if ! __has_feature(objc_arc)
+#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
+// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC
+#endif
+
+/**
+ * Does ARC support support GCD objects?
+ * It does if the minimum deployment target is iOS 6+ or Mac OS X 8+
+**/
+#if TARGET_OS_IPHONE
+
+ // Compiling for iOS
+
+ #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 0
+ #else // iOS 5.X or earlier
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 1
+ #endif
+
+#else
+
+ // Compiling for Mac OS X
+
+ #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 0
+ #else
+ #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier
+ #endif
+
+#endif
+
+#if TARGET_OS_IPHONE
+ #import <CFNetwork/CFNetwork.h>
+ #import <UIKit/UIKit.h>
+#endif
+
+#import <arpa/inet.h>
+#import <fcntl.h>
+#import <ifaddrs.h>
+#import <netdb.h>
+#import <net/if.h>
+#import <sys/socket.h>
+#import <sys/types.h>
+
+
+#if 0
+
+// Logging Enabled - See log level below
+
+// Logging uses the CocoaLumberjack framework (which is also GCD based).
+// http://code.google.com/p/cocoalumberjack/
+//
+// It allows us to do a lot of logging without significantly slowing down the code.
+#import "DDLog.h"
+
+#define LogAsync NO
+#define LogContext 65535
+
+#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
+#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__)
+
+#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+
+#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__)
+
+#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD)
+#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__)
+
+// Log levels : off, error, warn, info, verbose
+static const int logLevel = LOG_LEVEL_VERBOSE;
+
+#else
+
+// Logging Disabled
+
+#define LogError(frmt, ...) {}
+#define LogWarn(frmt, ...) {}
+#define LogInfo(frmt, ...) {}
+#define LogVerbose(frmt, ...) {}
+
+#define LogCError(frmt, ...) {}
+#define LogCWarn(frmt, ...) {}
+#define LogCInfo(frmt, ...) {}
+#define LogCVerbose(frmt, ...) {}
+
+#define LogTrace() {}
+#define LogCTrace(frmt, ...) {}
+
+#endif
+
+/**
+ * Seeing a return statements within an inner block
+ * can sometimes be mistaken for a return point of the enclosing method.
+ * This makes inline blocks a bit easier to read.
+**/
+#define return_from_block return
+
+/**
+ * A socket file descriptor is really just an integer.
+ * It represents the index of the socket within the kernel.
+ * This makes invalid file descriptor comparisons easier to read.
+**/
+#define SOCKET_NULL -1
+
+/**
+ * Just to type less code.
+**/
+#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }}
+
+
+@class GCDAsyncUdpSendPacket;
+
+NSString *const GCDAsyncUdpSocketException = @"GCDAsyncUdpSocketException";
+NSString *const GCDAsyncUdpSocketErrorDomain = @"GCDAsyncUdpSocketErrorDomain";
+
+NSString *const GCDAsyncUdpSocketQueueName = @"GCDAsyncUdpSocket";
+NSString *const GCDAsyncUdpSocketThreadName = @"GCDAsyncUdpSocket-CFStream";
+
+enum GCDAsyncUdpSocketFlags
+{
+ kDidCreateSockets = 1 << 0, // If set, the sockets have been created.
+ kDidBind = 1 << 1, // If set, bind has been called.
+ kConnecting = 1 << 2, // If set, a connection attempt is in progress.
+ kDidConnect = 1 << 3, // If set, socket is connected.
+ kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled
+ kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled
+ kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6.
+ kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4.
+ kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended.
+ kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended.
+ kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended.
+ kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended.
+ kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown.
+ kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown.
+ kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued.
+ kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued.
+ kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets.
+#if TARGET_OS_IPHONE
+ kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread
+#endif
+};
+
+enum GCDAsyncUdpSocketConfig
+{
+ kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled
+ kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled
+ kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6
+ kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@interface GCDAsyncUdpSocket ()
+{
+ id delegate;
+ dispatch_queue_t delegateQueue;
+
+ GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock;
+ dispatch_queue_t receiveFilterQueue;
+ BOOL receiveFilterAsync;
+
+ GCDAsyncUdpSocketSendFilterBlock sendFilterBlock;
+ dispatch_queue_t sendFilterQueue;
+ BOOL sendFilterAsync;
+
+ uint32_t flags;
+ uint16_t config;
+
+ uint16_t max4ReceiveSize;
+ uint32_t max6ReceiveSize;
+
+ int socket4FD;
+ int socket6FD;
+
+ dispatch_queue_t socketQueue;
+
+ dispatch_source_t send4Source;
+ dispatch_source_t send6Source;
+ dispatch_source_t receive4Source;
+ dispatch_source_t receive6Source;
+ dispatch_source_t sendTimer;
+
+ GCDAsyncUdpSendPacket *currentSend;
+ NSMutableArray *sendQueue;
+
+ unsigned long socket4FDBytesAvailable;
+ unsigned long socket6FDBytesAvailable;
+
+ uint32_t pendingFilterOperations;
+
+ NSData *cachedLocalAddress4;
+ NSString *cachedLocalHost4;
+ uint16_t cachedLocalPort4;
+
+ NSData *cachedLocalAddress6;
+ NSString *cachedLocalHost6;
+ uint16_t cachedLocalPort6;
+
+ NSData *cachedConnectedAddress;
+ NSString *cachedConnectedHost;
+ uint16_t cachedConnectedPort;
+ int cachedConnectedFamily;
+
+ void *IsOnSocketQueueOrTargetQueueKey;
+
+#if TARGET_OS_IPHONE
+ CFStreamClientContext streamContext;
+ CFReadStreamRef readStream4;
+ CFReadStreamRef readStream6;
+ CFWriteStreamRef writeStream4;
+ CFWriteStreamRef writeStream6;
+#endif
+
+ id userData;
+}
+
+- (void)resumeSend4Source;
+- (void)resumeSend6Source;
+- (void)resumeReceive4Source;
+- (void)resumeReceive6Source;
+- (void)closeSockets;
+
+- (void)maybeConnect;
+- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr;
+- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr;
+
+- (void)maybeDequeueSend;
+- (void)doPreSend;
+- (void)doSend;
+- (void)endCurrentSend;
+- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout;
+
+- (void)doReceive;
+- (void)doReceiveEOF;
+
+- (void)closeWithError:(NSError *)error;
+
+- (BOOL)performMulticastRequest:(int)requestType forGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr;
+
+#if TARGET_OS_IPHONE
+- (BOOL)createReadAndWriteStreams:(NSError **)errPtr;
+- (BOOL)registerForStreamCallbacks:(NSError **)errPtr;
+- (BOOL)addStreamsToRunLoop:(NSError **)errPtr;
+- (BOOL)openStreams:(NSError **)errPtr;
+- (void)removeStreamsFromRunLoop;
+- (void)closeReadAndWriteStreams;
+#endif
+
++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4;
++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6;
+
+#if TARGET_OS_IPHONE
+// Forward declaration
++ (void)listenerThread;
+#endif
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * The GCDAsyncUdpSendPacket encompasses the instructions for a single send/write.
+**/
+@interface GCDAsyncUdpSendPacket : NSObject {
+@public
+ NSData *buffer;
+ NSTimeInterval timeout;
+ long tag;
+
+ BOOL resolveInProgress;
+ BOOL filterInProgress;
+
+ NSArray *resolvedAddresses;
+ NSError *resolveError;
+
+ NSData *address;
+ int addressFamily;
+}
+
+- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i;
+
+@end
+
+@implementation GCDAsyncUdpSendPacket
+
+- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i
+{
+ if ((self = [super init]))
+ {
+ buffer = d;
+ timeout = t;
+ tag = i;
+
+ resolveInProgress = NO;
+ }
+ return self;
+}
+
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@interface GCDAsyncUdpSpecialPacket : NSObject {
+@public
+// uint8_t type;
+
+ BOOL resolveInProgress;
+
+ NSArray *addresses;
+ NSError *error;
+}
+
+- (id)init;
+
+@end
+
+@implementation GCDAsyncUdpSpecialPacket
+
+- (id)init
+{
+ self = [super init];
+ return self;
+}
+
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@implementation GCDAsyncUdpSocket
+
+- (id)init
+{
+ LogTrace();
+
+ return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL];
+}
+
+- (id)initWithSocketQueue:(dispatch_queue_t)sq
+{
+ LogTrace();
+
+ return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq];
+}
+
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq
+{
+ LogTrace();
+
+ return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL];
+}
+
+- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq
+{
+ LogTrace();
+
+ if ((self = [super init]))
+ {
+ delegate = aDelegate;
+
+ if (dq)
+ {
+ delegateQueue = dq;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_retain(delegateQueue);
+ #endif
+ }
+
+ max4ReceiveSize = 9216;
+ max6ReceiveSize = 9216;
+
+ socket4FD = SOCKET_NULL;
+ socket6FD = SOCKET_NULL;
+
+ if (sq)
+ {
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+ NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
+ @"The given socketQueue parameter must not be a concurrent queue.");
+
+ socketQueue = sq;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_retain(socketQueue);
+ #endif
+ }
+ else
+ {
+ socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], NULL);
+ }
+
+ // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
+ // From the documentation:
+ //
+ // > Keys are only compared as pointers and are never dereferenced.
+ // > Thus, you can use a pointer to a static variable for a specific subsystem or
+ // > any other value that allows you to identify the value uniquely.
+ //
+ // We're just going to use the memory address of an ivar.
+ // Specifically an ivar that is explicitly named for our purpose to make the code more readable.
+ //
+ // However, it feels tedious (and less readable) to include the "&" all the time:
+ // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey)
+ //
+ // So we're going to make it so it doesn't matter if we use the '&' or not,
+ // by assigning the value of the ivar to the address of the ivar.
+ // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey;
+
+ IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey;
+
+ void *nonNullUnusedPointer = (__bridge void *)self;
+ dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
+
+ currentSend = nil;
+ sendQueue = [[NSMutableArray alloc] initWithCapacity:5];
+
+ #if TARGET_OS_IPHONE
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationWillEnterForeground:)
+ name:UIApplicationWillEnterForegroundNotification
+ object:nil];
+ #endif
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ LogInfo(@"%@ - %@ (start)", THIS_METHOD, self);
+
+#if TARGET_OS_IPHONE
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+#endif
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ [self closeWithError:nil];
+ }
+ else
+ {
+ dispatch_sync(socketQueue, ^{
+ [self closeWithError:nil];
+ });
+ }
+
+ delegate = nil;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ #endif
+ delegateQueue = NULL;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (socketQueue) dispatch_release(socketQueue);
+ #endif
+ socketQueue = NULL;
+
+ LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Configuration
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (id)delegate
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return delegate;
+ }
+ else
+ {
+ __block id result = nil;
+
+ dispatch_sync(socketQueue, ^{
+ result = delegate;
+ });
+
+ return result;
+ }
+}
+
+- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+ delegate = newDelegate;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegate:(id)newDelegate
+{
+ [self setDelegate:newDelegate synchronously:NO];
+}
+
+- (void)synchronouslySetDelegate:(id)newDelegate
+{
+ [self setDelegate:newDelegate synchronously:YES];
+}
+
+- (dispatch_queue_t)delegateQueue
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ return delegateQueue;
+ }
+ else
+ {
+ __block dispatch_queue_t result = NULL;
+
+ dispatch_sync(socketQueue, ^{
+ result = delegateQueue;
+ });
+
+ return result;
+ }
+}
+
+- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ if (newDelegateQueue) dispatch_retain(newDelegateQueue);
+ #endif
+
+ delegateQueue = newDelegateQueue;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegateQueue:newDelegateQueue synchronously:NO];
+}
+
+- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegateQueue:newDelegateQueue synchronously:YES];
+}
+
+- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ if (delegatePtr) *delegatePtr = delegate;
+ if (delegateQueuePtr) *delegateQueuePtr = delegateQueue;
+ }
+ else
+ {
+ __block id dPtr = NULL;
+ __block dispatch_queue_t dqPtr = NULL;
+
+ dispatch_sync(socketQueue, ^{
+ dPtr = delegate;
+ dqPtr = delegateQueue;
+ });
+
+ if (delegatePtr) *delegatePtr = dPtr;
+ if (delegateQueuePtr) *delegateQueuePtr = dqPtr;
+ }
+}
+
+- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously
+{
+ dispatch_block_t block = ^{
+
+ delegate = newDelegate;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (delegateQueue) dispatch_release(delegateQueue);
+ if (newDelegateQueue) dispatch_retain(newDelegateQueue);
+ #endif
+
+ delegateQueue = newDelegateQueue;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) {
+ block();
+ }
+ else {
+ if (synchronously)
+ dispatch_sync(socketQueue, block);
+ else
+ dispatch_async(socketQueue, block);
+ }
+}
+
+- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO];
+}
+
+- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue
+{
+ [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES];
+}
+
+- (BOOL)isIPv4Enabled
+{
+ // Note: YES means kIPv4Disabled is OFF
+
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+
+ result = ((config & kIPv4Disabled) == 0);
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setIPv4Enabled:(BOOL)flag
+{
+ // Note: YES means kIPv4Disabled is OFF
+
+ dispatch_block_t block = ^{
+
+ LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO"));
+
+ if (flag)
+ config &= ~kIPv4Disabled;
+ else
+ config |= kIPv4Disabled;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (BOOL)isIPv6Enabled
+{
+ // Note: YES means kIPv6Disabled is OFF
+
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+
+ result = ((config & kIPv6Disabled) == 0);
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setIPv6Enabled:(BOOL)flag
+{
+ // Note: YES means kIPv6Disabled is OFF
+
+ dispatch_block_t block = ^{
+
+ LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO"));
+
+ if (flag)
+ config &= ~kIPv6Disabled;
+ else
+ config |= kIPv6Disabled;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (BOOL)isIPv4Preferred
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (config & kPreferIPv4) ? YES : NO;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isIPv6Preferred
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (config & kPreferIPv6) ? YES : NO;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isIPVersionNeutral
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (config & (kPreferIPv4 | kPreferIPv6)) == 0;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setPreferIPv4
+{
+ dispatch_block_t block = ^{
+
+ LogTrace();
+
+ config |= kPreferIPv4;
+ config &= ~kPreferIPv6;
+
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (void)setPreferIPv6
+{
+ dispatch_block_t block = ^{
+
+ LogTrace();
+
+ config &= ~kPreferIPv4;
+ config |= kPreferIPv6;
+
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (void)setIPVersionNeutral
+{
+ dispatch_block_t block = ^{
+
+ LogTrace();
+
+ config &= ~kPreferIPv4;
+ config &= ~kPreferIPv6;
+
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (uint16_t)maxReceiveIPv4BufferSize
+{
+ __block uint16_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ result = max4ReceiveSize;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max
+{
+ dispatch_block_t block = ^{
+
+ LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max);
+
+ max4ReceiveSize = max;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (uint32_t)maxReceiveIPv6BufferSize
+{
+ __block uint32_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ result = max6ReceiveSize;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max
+{
+ dispatch_block_t block = ^{
+
+ LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max);
+
+ max6ReceiveSize = max;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+
+- (id)userData
+{
+ __block id result = nil;
+
+ dispatch_block_t block = ^{
+
+ result = userData;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (void)setUserData:(id)arbitraryUserData
+{
+ dispatch_block_t block = ^{
+
+ if (userData != arbitraryUserData)
+ {
+ userData = arbitraryUserData;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Delegate Helpers
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)notifyDidConnectToAddress:(NSData *)anAddress
+{
+ LogTrace();
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)])
+ {
+ id theDelegate = delegate;
+ NSData *address = [anAddress copy]; // In case param is NSMutableData
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocket:self didConnectToAddress:address];
+ }});
+ }
+}
+
+- (void)notifyDidNotConnect:(NSError *)error
+{
+ LogTrace();
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(udpSocket:didNotConnect:)])
+ {
+ id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocket:self didNotConnect:error];
+ }});
+ }
+}
+
+- (void)notifyDidSendDataWithTag:(long)tag
+{
+ LogTrace();
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)])
+ {
+ id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocket:self didSendDataWithTag:tag];
+ }});
+ }
+}
+
+- (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error
+{
+ LogTrace();
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)])
+ {
+ id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error];
+ }});
+ }
+}
+
+- (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context
+{
+ LogTrace();
+
+ SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:);
+
+ if (delegateQueue && [delegate respondsToSelector:selector])
+ {
+ id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context];
+ }});
+ }
+}
+
+- (void)notifyDidCloseWithError:(NSError *)error
+{
+ LogTrace();
+
+ if (delegateQueue && [delegate respondsToSelector:@selector(udpSocketDidClose:withError:)])
+ {
+ id theDelegate = delegate;
+
+ dispatch_async(delegateQueue, ^{ @autoreleasepool {
+
+ [theDelegate udpSocketDidClose:self withError:error];
+ }});
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Errors
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (NSError *)badConfigError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain
+ code:GCDAsyncUdpSocketBadConfigError
+ userInfo:userInfo];
+}
+
+- (NSError *)badParamError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain
+ code:GCDAsyncUdpSocketBadParamError
+ userInfo:userInfo];
+}
+
+- (NSError *)gaiError:(int)gai_error
+{
+ NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding];
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo];
+}
+
+- (NSError *)errnoErrorWithReason:(NSString *)reason
+{
+ NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)];
+ NSDictionary *userInfo;
+
+ if (reason)
+ userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey,
+ reason, NSLocalizedFailureReasonErrorKey, nil];
+ else
+ userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, nil];
+
+ return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo];
+}
+
+- (NSError *)errnoError
+{
+ return [self errnoErrorWithReason:nil];
+}
+
+/**
+ * Returns a standard send timeout error.
+**/
+- (NSError *)sendTimeoutError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError",
+ @"GCDAsyncUdpSocket", [NSBundle mainBundle],
+ @"Send operation timed out", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain
+ code:GCDAsyncUdpSocketSendTimeoutError
+ userInfo:userInfo];
+}
+
+- (NSError *)socketClosedError
+{
+ NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError",
+ @"GCDAsyncUdpSocket", [NSBundle mainBundle],
+ @"Socket closed", nil);
+
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo];
+}
+
+- (NSError *)otherError:(NSString *)errMsg
+{
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey];
+
+ return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain
+ code:GCDAsyncUdpSocketOtherError
+ userInfo:userInfo];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Utilities
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)preOp:(NSError **)errPtr
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if (delegate == nil) // Must have delegate set
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if (delegateQueue == NULL) // Must have delegate queue set
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ return YES;
+}
+
+/**
+ * This method executes on a global concurrent queue.
+ * When complete, it executes the given completion block on the socketQueue.
+**/
+- (void)asyncResolveHost:(NSString *)aHost
+ port:(uint16_t)port
+ withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock
+{
+ LogTrace();
+
+ // Check parameter(s)
+
+ if (aHost == nil)
+ {
+ NSString *msg = @"The host param is nil. Should be domain name or IP address string.";
+ NSError *error = [self badParamError:msg];
+
+ // We should still use dispatch_async since this method is expected to be asynchronous
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ completionBlock(nil, error);
+ }});
+
+ return;
+ }
+
+ // It's possible that the given aHost parameter is actually a NSMutableString.
+ // So we want to copy it now, within this block that will be executed synchronously.
+ // This way the asynchronous lookup block below doesn't have to worry about it changing.
+
+ NSString *host = [aHost copy];
+
+
+ dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
+ dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool {
+
+ NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2];
+ NSError *error = nil;
+
+ if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"])
+ {
+ // Use LOOPBACK address
+ struct sockaddr_in sockaddr4;
+ memset(&sockaddr4, 0, sizeof(sockaddr4));
+
+ sockaddr4.sin_len = sizeof(struct sockaddr_in);
+ sockaddr4.sin_family = AF_INET;
+ sockaddr4.sin_port = htons(port);
+ sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ struct sockaddr_in6 sockaddr6;
+ memset(&sockaddr6, 0, sizeof(sockaddr6));
+
+ sockaddr6.sin6_len = sizeof(struct sockaddr_in6);
+ sockaddr6.sin6_family = AF_INET6;
+ sockaddr6.sin6_port = htons(port);
+ sockaddr6.sin6_addr = in6addr_loopback;
+
+ // Wrap the native address structures and add to list
+ [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]];
+ [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]];
+ }
+ else
+ {
+ NSString *portStr = [NSString stringWithFormat:@"%hu", port];
+
+ struct addrinfo hints, *res, *res0;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = PF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_protocol = IPPROTO_UDP;
+
+ int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
+
+ if (gai_error)
+ {
+ error = [self gaiError:gai_error];
+ }
+ else
+ {
+ for(res = res0; res; res = res->ai_next)
+ {
+ if (res->ai_family == AF_INET)
+ {
+ // Found IPv4 address
+ // Wrap the native address structure and add to list
+
+ [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]];
+ }
+ else if (res->ai_family == AF_INET6)
+ {
+ // Found IPv6 address
+ // Wrap the native address structure and add to list
+
+ [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]];
+ }
+ }
+ freeaddrinfo(res0);
+
+ if ([addresses count] == 0)
+ {
+ error = [self gaiError:EAI_FAIL];
+ }
+ }
+ }
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ completionBlock(addresses, error);
+ }});
+
+ }});
+}
+
+/**
+ * This method picks an address from the given list of addresses.
+ * The address picked depends upon which protocols are disabled, deactived, & preferred.
+ *
+ * Returns the address family (AF_INET or AF_INET6) of the picked address,
+ * or AF_UNSPEC and the corresponding error is there's a problem.
+**/
+- (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses:(NSArray *)addresses
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert([addresses count] > 0, @"Expected at least one address");
+
+ int resultAF = AF_UNSPEC;
+ NSData *resultAddress = nil;
+ NSError *resultError = nil;
+
+ // Check for problems
+
+ BOOL resolvedIPv4Address = NO;
+ BOOL resolvedIPv6Address = NO;
+
+ for (NSData *address in addresses)
+ {
+ switch ([[self class] familyFromAddress:address])
+ {
+ case AF_INET : resolvedIPv4Address = YES; break;
+ case AF_INET6 : resolvedIPv6Address = YES; break;
+
+ default : NSAssert(NO, @"Addresses array contains invalid address");
+ }
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && !resolvedIPv6Address)
+ {
+ NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es).";
+ resultError = [self otherError:msg];
+
+ if (addressPtr) *addressPtr = resultAddress;
+ if (errorPtr) *errorPtr = resultError;
+
+ return resultAF;
+ }
+
+ if (isIPv6Disabled && !resolvedIPv4Address)
+ {
+ NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es).";
+ resultError = [self otherError:msg];
+
+ if (addressPtr) *addressPtr = resultAddress;
+ if (errorPtr) *errorPtr = resultError;
+
+ return resultAF;
+ }
+
+ BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO;
+ BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO;
+
+ if (isIPv4Deactivated && !resolvedIPv6Address)
+ {
+ NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es).";
+ resultError = [self otherError:msg];
+
+ if (addressPtr) *addressPtr = resultAddress;
+ if (errorPtr) *errorPtr = resultError;
+
+ return resultAF;
+ }
+
+ if (isIPv6Deactivated && !resolvedIPv4Address)
+ {
+ NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es).";
+ resultError = [self otherError:msg];
+
+ if (addressPtr) *addressPtr = resultAddress;
+ if (errorPtr) *errorPtr = resultError;
+
+ return resultAF;
+ }
+
+ // Extract first IPv4 and IPv6 address in list
+
+ BOOL ipv4WasFirstInList = YES;
+ NSData *address4 = nil;
+ NSData *address6 = nil;
+
+ for (NSData *address in addresses)
+ {
+ int af = [[self class] familyFromAddress:address];
+
+ if (af == AF_INET)
+ {
+ if (address4 == nil)
+ {
+ address4 = address;
+
+ if (address6)
+ break;
+ else
+ ipv4WasFirstInList = YES;
+ }
+ }
+ else // af == AF_INET6
+ {
+ if (address6 == nil)
+ {
+ address6 = address;
+
+ if (address4)
+ break;
+ else
+ ipv4WasFirstInList = NO;
+ }
+ }
+ }
+
+ // Determine socket type
+
+ BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO;
+ BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO;
+
+ BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil));
+ BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil));
+
+ NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state");
+ NSAssert(!(useIPv4 && useIPv6), @"Invalid logic");
+
+ if (useIPv4 || (!useIPv6 && ipv4WasFirstInList))
+ {
+ resultAF = AF_INET;
+ resultAddress = address4;
+ }
+ else
+ {
+ resultAF = AF_INET6;
+ resultAddress = address6;
+ }
+
+ if (addressPtr) *addressPtr = resultAddress;
+ if (errorPtr) *errorPtr = resultError;
+
+ return resultAF;
+}
+
+/**
+ * Finds the address(es) of an interface description.
+ * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34).
+**/
+- (void)convertIntefaceDescription:(NSString *)interfaceDescription
+ port:(uint16_t)port
+ intoAddress4:(NSData **)interfaceAddr4Ptr
+ address6:(NSData **)interfaceAddr6Ptr
+{
+ NSData *addr4 = nil;
+ NSData *addr6 = nil;
+
+ if (interfaceDescription == nil)
+ {
+ // ANY address
+
+ struct sockaddr_in sockaddr4;
+ memset(&sockaddr4, 0, sizeof(sockaddr4));
+
+ sockaddr4.sin_len = sizeof(sockaddr4);
+ sockaddr4.sin_family = AF_INET;
+ sockaddr4.sin_port = htons(port);
+ sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ struct sockaddr_in6 sockaddr6;
+ memset(&sockaddr6, 0, sizeof(sockaddr6));
+
+ sockaddr6.sin6_len = sizeof(sockaddr6);
+ sockaddr6.sin6_family = AF_INET6;
+ sockaddr6.sin6_port = htons(port);
+ sockaddr6.sin6_addr = in6addr_any;
+
+ addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
+ addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
+ }
+ else if ([interfaceDescription isEqualToString:@"localhost"] ||
+ [interfaceDescription isEqualToString:@"loopback"])
+ {
+ // LOOPBACK address
+
+ struct sockaddr_in sockaddr4;
+ memset(&sockaddr4, 0, sizeof(sockaddr4));
+
+ sockaddr4.sin_len = sizeof(struct sockaddr_in);
+ sockaddr4.sin_family = AF_INET;
+ sockaddr4.sin_port = htons(port);
+ sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ struct sockaddr_in6 sockaddr6;
+ memset(&sockaddr6, 0, sizeof(sockaddr6));
+
+ sockaddr6.sin6_len = sizeof(struct sockaddr_in6);
+ sockaddr6.sin6_family = AF_INET6;
+ sockaddr6.sin6_port = htons(port);
+ sockaddr6.sin6_addr = in6addr_loopback;
+
+ addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)];
+ addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)];
+ }
+ else
+ {
+ const char *iface = [interfaceDescription UTF8String];
+
+ struct ifaddrs *addrs;
+ const struct ifaddrs *cursor;
+
+ if ((getifaddrs(&addrs) == 0))
+ {
+ cursor = addrs;
+ while (cursor != NULL)
+ {
+ if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET))
+ {
+ // IPv4
+
+ struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr;
+
+ if (strcmp(cursor->ifa_name, iface) == 0)
+ {
+ // Name match
+
+ struct sockaddr_in nativeAddr4 = *addr;
+ nativeAddr4.sin_port = htons(port);
+
+ addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
+ }
+ else
+ {
+ char ip[INET_ADDRSTRLEN];
+
+ const char *conversion;
+ conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip));
+
+ if ((conversion != NULL) && (strcmp(ip, iface) == 0))
+ {
+ // IP match
+
+ struct sockaddr_in nativeAddr4 = *addr;
+ nativeAddr4.sin_port = htons(port);
+
+ addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
+ }
+ }
+ }
+ else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6))
+ {
+ // IPv6
+
+ struct sockaddr_in6 *addr = (struct sockaddr_in6 *)cursor->ifa_addr;
+
+ if (strcmp(cursor->ifa_name, iface) == 0)
+ {
+ // Name match
+
+ struct sockaddr_in6 nativeAddr6 = *addr;
+ nativeAddr6.sin6_port = htons(port);
+
+ addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
+ }
+ else
+ {
+ char ip[INET6_ADDRSTRLEN];
+
+ const char *conversion;
+ conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip));
+
+ if ((conversion != NULL) && (strcmp(ip, iface) == 0))
+ {
+ // IP match
+
+ struct sockaddr_in6 nativeAddr6 = *addr;
+ nativeAddr6.sin6_port = htons(port);
+
+ addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
+ }
+ }
+ }
+
+ cursor = cursor->ifa_next;
+ }
+
+ freeifaddrs(addrs);
+ }
+ }
+
+ if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4;
+ if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6;
+}
+
+/**
+ * Converts a numeric hostname into its corresponding address.
+ * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34)
+**/
+- (void)convertNumericHost:(NSString *)numericHost
+ port:(uint16_t)port
+ intoAddress4:(NSData **)addr4Ptr
+ address6:(NSData **)addr6Ptr
+{
+ NSData *addr4 = nil;
+ NSData *addr6 = nil;
+
+ if (numericHost)
+ {
+ NSString *portStr = [NSString stringWithFormat:@"%hu", port];
+
+ struct addrinfo hints, *res, *res0;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = PF_UNSPEC;
+ hints.ai_socktype = SOCK_DGRAM;
+ hints.ai_protocol = IPPROTO_UDP;
+ hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted
+
+ if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0)
+ {
+ for (res = res0; res; res = res->ai_next)
+ {
+ if ((addr4 == nil) && (res->ai_family == AF_INET))
+ {
+ // Found IPv4 address
+ // Wrap the native address structure
+ addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
+ }
+ else if ((addr6 == nil) && (res->ai_family == AF_INET6))
+ {
+ // Found IPv6 address
+ // Wrap the native address structure
+ addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
+ }
+ }
+ freeaddrinfo(res0);
+ }
+ }
+
+ if (addr4Ptr) *addr4Ptr = addr4;
+ if (addr6Ptr) *addr6Ptr = addr6;
+}
+
+- (BOOL)isConnectedToAddress4:(NSData *)someAddr4
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(flags & kDidConnect, @"Not connected");
+ NSAssert(cachedConnectedAddress, @"Expected cached connected address");
+
+ if (cachedConnectedFamily != AF_INET)
+ {
+ return NO;
+ }
+
+ const struct sockaddr_in *sSockaddr4 = (struct sockaddr_in *)[someAddr4 bytes];
+ const struct sockaddr_in *cSockaddr4 = (struct sockaddr_in *)[cachedConnectedAddress bytes];
+
+ if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0)
+ {
+ return NO;
+ }
+ if (memcmp(&sSockaddr4->sin_port, &cSockaddr4->sin_port, sizeof(in_port_t)) != 0)
+ {
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)isConnectedToAddress6:(NSData *)someAddr6
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(flags & kDidConnect, @"Not connected");
+ NSAssert(cachedConnectedAddress, @"Expected cached connected address");
+
+ if (cachedConnectedFamily != AF_INET6)
+ {
+ return NO;
+ }
+
+ const struct sockaddr_in6 *sSockaddr6 = (struct sockaddr_in6 *)[someAddr6 bytes];
+ const struct sockaddr_in6 *cSockaddr6 = (struct sockaddr_in6 *)[cachedConnectedAddress bytes];
+
+ if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0)
+ {
+ return NO;
+ }
+ if (memcmp(&sSockaddr6->sin6_port, &cSockaddr6->sin6_port, sizeof(in_port_t)) != 0)
+ {
+ return NO;
+ }
+
+ return YES;
+}
+
+- (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4
+{
+ if (interfaceAddr4 == nil)
+ return 0;
+ if ([interfaceAddr4 length] != sizeof(struct sockaddr_in))
+ return 0;
+
+ int result = 0;
+ struct sockaddr_in *ifaceAddr = (struct sockaddr_in *)[interfaceAddr4 bytes];
+
+ struct ifaddrs *addrs;
+ const struct ifaddrs *cursor;
+
+ if ((getifaddrs(&addrs) == 0))
+ {
+ cursor = addrs;
+ while (cursor != NULL)
+ {
+ if (cursor->ifa_addr->sa_family == AF_INET)
+ {
+ // IPv4
+
+ struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr;
+
+ if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0)
+ {
+ result = if_nametoindex(cursor->ifa_name);
+ break;
+ }
+ }
+
+ cursor = cursor->ifa_next;
+ }
+
+ freeifaddrs(addrs);
+ }
+
+ return result;
+}
+
+- (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6
+{
+ if (interfaceAddr6 == nil)
+ return 0;
+ if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6))
+ return 0;
+
+ int result = 0;
+ struct sockaddr_in6 *ifaceAddr = (struct sockaddr_in6 *)[interfaceAddr6 bytes];
+
+ struct ifaddrs *addrs;
+ const struct ifaddrs *cursor;
+
+ if ((getifaddrs(&addrs) == 0))
+ {
+ cursor = addrs;
+ while (cursor != NULL)
+ {
+ if (cursor->ifa_addr->sa_family == AF_INET6)
+ {
+ // IPv6
+
+ struct sockaddr_in6 *addr = (struct sockaddr_in6 *)cursor->ifa_addr;
+
+ if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0)
+ {
+ result = if_nametoindex(cursor->ifa_name);
+ break;
+ }
+ }
+
+ cursor = cursor->ifa_next;
+ }
+
+ freeifaddrs(addrs);
+ }
+
+ return result;
+}
+
+- (void)setupSendAndReceiveSourcesForSocket4
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue);
+ receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
+
+ // Setup event handlers
+
+ dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"send4EventBlock");
+ LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source));
+
+ flags |= kSock4CanAcceptBytes;
+
+ // If we're ready to send data, do so immediately.
+ // Otherwise pause the send source or it will continue to fire over and over again.
+
+ if (currentSend == nil)
+ {
+ LogVerbose(@"Nothing to send");
+ [self suspendSend4Source];
+ }
+ else if (currentSend->resolveInProgress)
+ {
+ LogVerbose(@"currentSend - waiting for address resolve");
+ [self suspendSend4Source];
+ }
+ else if (currentSend->filterInProgress)
+ {
+ LogVerbose(@"currentSend - waiting on sendFilter");
+ [self suspendSend4Source];
+ }
+ else
+ {
+ [self doSend];
+ }
+
+ }});
+
+ dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"receive4EventBlock");
+
+ socket4FDBytesAvailable = dispatch_source_get_data(receive4Source);
+ LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable);
+
+ if (socket4FDBytesAvailable > 0)
+ [self doReceive];
+ else
+ [self doReceiveEOF];
+
+ }});
+
+ // Setup cancel handlers
+
+ __block int socketFDRefCount = 2;
+
+ int theSocketFD = socket4FD;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theSendSource = send4Source;
+ dispatch_source_t theReceiveSource = receive4Source;
+ #endif
+
+ dispatch_source_set_cancel_handler(send4Source, ^{
+
+ LogVerbose(@"send4CancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(send4Source)");
+ dispatch_release(theSendSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socket4FD)");
+ close(theSocketFD);
+ }
+ });
+
+ dispatch_source_set_cancel_handler(receive4Source, ^{
+
+ LogVerbose(@"receive4CancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(receive4Source)");
+ dispatch_release(theReceiveSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socket4FD)");
+ close(theSocketFD);
+ }
+ });
+
+ // We will not be able to receive until the socket is bound to a port,
+ // either explicitly via bind, or implicitly by connect or by sending data.
+ //
+ // But we should be able to send immediately.
+
+ socket4FDBytesAvailable = 0;
+ flags |= kSock4CanAcceptBytes;
+
+ flags |= kSend4SourceSuspended;
+ flags |= kReceive4SourceSuspended;
+}
+
+- (void)setupSendAndReceiveSourcesForSocket6
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue);
+ receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue);
+
+ // Setup event handlers
+
+ dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"send6EventBlock");
+ LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source));
+
+ flags |= kSock6CanAcceptBytes;
+
+ // If we're ready to send data, do so immediately.
+ // Otherwise pause the send source or it will continue to fire over and over again.
+
+ if (currentSend == nil)
+ {
+ LogVerbose(@"Nothing to send");
+ [self suspendSend6Source];
+ }
+ else if (currentSend->resolveInProgress)
+ {
+ LogVerbose(@"currentSend - waiting for address resolve");
+ [self suspendSend6Source];
+ }
+ else if (currentSend->filterInProgress)
+ {
+ LogVerbose(@"currentSend - waiting on sendFilter");
+ [self suspendSend6Source];
+ }
+ else
+ {
+ [self doSend];
+ }
+
+ }});
+
+ dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool {
+
+ LogVerbose(@"receive6EventBlock");
+
+ socket6FDBytesAvailable = dispatch_source_get_data(receive6Source);
+ LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable);
+
+ if (socket6FDBytesAvailable > 0)
+ [self doReceive];
+ else
+ [self doReceiveEOF];
+
+ }});
+
+ // Setup cancel handlers
+
+ __block int socketFDRefCount = 2;
+
+ int theSocketFD = socket6FD;
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_source_t theSendSource = send6Source;
+ dispatch_source_t theReceiveSource = receive6Source;
+ #endif
+
+ dispatch_source_set_cancel_handler(send6Source, ^{
+
+ LogVerbose(@"send6CancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(send6Source)");
+ dispatch_release(theSendSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socket6FD)");
+ close(theSocketFD);
+ }
+ });
+
+ dispatch_source_set_cancel_handler(receive6Source, ^{
+
+ LogVerbose(@"receive6CancelBlock");
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ LogVerbose(@"dispatch_release(receive6Source)");
+ dispatch_release(theReceiveSource);
+ #endif
+
+ if (--socketFDRefCount == 0)
+ {
+ LogVerbose(@"close(socket6FD)");
+ close(theSocketFD);
+ }
+ });
+
+ // We will not be able to receive until the socket is bound to a port,
+ // either explicitly via bind, or implicitly by connect or by sending data.
+ //
+ // But we should be able to send immediately.
+
+ socket6FDBytesAvailable = 0;
+ flags |= kSock6CanAcceptBytes;
+
+ flags |= kSend6SourceSuspended;
+ flags |= kReceive6SourceSuspended;
+}
+
+- (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError **)errPtr
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created");
+
+ // CreateSocket Block
+ // This block will be invoked below.
+
+ int(^createSocket)(int) = ^int (int domain) {
+
+ int socketFD = socket(domain, SOCK_DGRAM, 0);
+
+ if (socketFD == SOCKET_NULL)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error in socket() function"];
+
+ return SOCKET_NULL;
+ }
+
+ int status;
+
+ // Set socket options
+
+ status = fcntl(socketFD, F_SETFL, O_NONBLOCK);
+ if (status == -1)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"];
+
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ int reuseaddr = 1;
+ status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr));
+ if (status == -1)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"];
+
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ int nosigpipe = 1;
+ status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));
+ if (status == -1)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"];
+
+ close(socketFD);
+ return SOCKET_NULL;
+ }
+
+ return socketFD;
+ };
+
+ // Create sockets depending upon given configuration.
+
+ if (useIPv4)
+ {
+ LogVerbose(@"Creating IPv4 socket");
+
+ socket4FD = createSocket(AF_INET);
+ if (socket4FD == SOCKET_NULL)
+ {
+ // errPtr set in local createSocket() block
+ return NO;
+ }
+ }
+
+ if (useIPv6)
+ {
+ LogVerbose(@"Creating IPv6 socket");
+
+ socket6FD = createSocket(AF_INET6);
+ if (socket6FD == SOCKET_NULL)
+ {
+ // errPtr set in local createSocket() block
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ close(socket4FD);
+ socket4FD = SOCKET_NULL;
+ }
+
+ return NO;
+ }
+ }
+
+ // Setup send and receive sources
+
+ if (useIPv4)
+ [self setupSendAndReceiveSourcesForSocket4];
+ if (useIPv6)
+ [self setupSendAndReceiveSourcesForSocket6];
+
+ flags |= kDidCreateSockets;
+ return YES;
+}
+
+- (BOOL)createSockets:(NSError **)errPtr
+{
+ LogTrace();
+
+ BOOL useIPv4 = [self isIPv4Enabled];
+ BOOL useIPv6 = [self isIPv6Enabled];
+
+ return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr];
+}
+
+- (void)suspendSend4Source
+{
+ if (send4Source && !(flags & kSend4SourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(send4Source)");
+
+ dispatch_suspend(send4Source);
+ flags |= kSend4SourceSuspended;
+ }
+}
+
+- (void)suspendSend6Source
+{
+ if (send6Source && !(flags & kSend6SourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(send6Source)");
+
+ dispatch_suspend(send6Source);
+ flags |= kSend6SourceSuspended;
+ }
+}
+
+- (void)resumeSend4Source
+{
+ if (send4Source && (flags & kSend4SourceSuspended))
+ {
+ LogVerbose(@"dispatch_resume(send4Source)");
+
+ dispatch_resume(send4Source);
+ flags &= ~kSend4SourceSuspended;
+ }
+}
+
+- (void)resumeSend6Source
+{
+ if (send6Source && (flags & kSend6SourceSuspended))
+ {
+ LogVerbose(@"dispatch_resume(send6Source)");
+
+ dispatch_resume(send6Source);
+ flags &= ~kSend6SourceSuspended;
+ }
+}
+
+- (void)suspendReceive4Source
+{
+ if (receive4Source && !(flags & kReceive4SourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(receive4Source)");
+
+ dispatch_suspend(receive4Source);
+ flags |= kReceive4SourceSuspended;
+ }
+}
+
+- (void)suspendReceive6Source
+{
+ if (receive6Source && !(flags & kReceive6SourceSuspended))
+ {
+ LogVerbose(@"dispatch_suspend(receive6Source)");
+
+ dispatch_suspend(receive6Source);
+ flags |= kReceive6SourceSuspended;
+ }
+}
+
+- (void)resumeReceive4Source
+{
+ if (receive4Source && (flags & kReceive4SourceSuspended))
+ {
+ LogVerbose(@"dispatch_resume(receive4Source)");
+
+ dispatch_resume(receive4Source);
+ flags &= ~kReceive4SourceSuspended;
+ }
+}
+
+- (void)resumeReceive6Source
+{
+ if (receive6Source && (flags & kReceive6SourceSuspended))
+ {
+ LogVerbose(@"dispatch_resume(receive6Source)");
+
+ dispatch_resume(receive6Source);
+ flags &= ~kReceive6SourceSuspended;
+ }
+}
+
+- (void)closeSocket4
+{
+ if (socket4FD != SOCKET_NULL)
+ {
+ LogVerbose(@"dispatch_source_cancel(send4Source)");
+ dispatch_source_cancel(send4Source);
+
+ LogVerbose(@"dispatch_source_cancel(receive4Source)");
+ dispatch_source_cancel(receive4Source);
+
+ // For some crazy reason (in my opinion), cancelling a dispatch source doesn't
+ // invoke the cancel handler if the dispatch source is paused.
+ // So we have to unpause the source if needed.
+ // This allows the cancel handler to be run, which in turn releases the source and closes the socket.
+
+ [self resumeSend4Source];
+ [self resumeReceive4Source];
+
+ // The sockets will be closed by the cancel handlers of the corresponding source
+
+ send4Source = NULL;
+ receive4Source = NULL;
+
+ socket4FD = SOCKET_NULL;
+
+ // Clear socket states
+
+ socket4FDBytesAvailable = 0;
+ flags &= ~kSock4CanAcceptBytes;
+
+ // Clear cached info
+
+ cachedLocalAddress4 = nil;
+ cachedLocalHost4 = nil;
+ cachedLocalPort4 = 0;
+ }
+}
+
+- (void)closeSocket6
+{
+ if (socket6FD != SOCKET_NULL)
+ {
+ LogVerbose(@"dispatch_source_cancel(send6Source)");
+ dispatch_source_cancel(send6Source);
+
+ LogVerbose(@"dispatch_source_cancel(receive6Source)");
+ dispatch_source_cancel(receive6Source);
+
+ // For some crazy reason (in my opinion), cancelling a dispatch source doesn't
+ // invoke the cancel handler if the dispatch source is paused.
+ // So we have to unpause the source if needed.
+ // This allows the cancel handler to be run, which in turn releases the source and closes the socket.
+
+ [self resumeSend6Source];
+ [self resumeReceive6Source];
+
+ send6Source = NULL;
+ receive6Source = NULL;
+
+ // The sockets will be closed by the cancel handlers of the corresponding source
+
+ socket6FD = SOCKET_NULL;
+
+ // Clear socket states
+
+ socket6FDBytesAvailable = 0;
+ flags &= ~kSock6CanAcceptBytes;
+
+ // Clear cached info
+
+ cachedLocalAddress6 = nil;
+ cachedLocalHost6 = nil;
+ cachedLocalPort6 = 0;
+ }
+}
+
+- (void)closeSockets
+{
+ [self closeSocket4];
+ [self closeSocket6];
+
+ flags &= ~kDidCreateSockets;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Diagnostics
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)getLocalAddress:(NSData **)dataPtr
+ host:(NSString **)hostPtr
+ port:(uint16_t *)portPtr
+ forSocket:(int)socketFD
+ withFamily:(int)socketFamily
+{
+
+ NSData *data = nil;
+ NSString *host = nil;
+ uint16_t port = 0;
+
+ if (socketFamily == AF_INET)
+ {
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
+ {
+ data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len];
+ host = [[self class] hostFromSockaddr4:&sockaddr4];
+ port = [[self class] portFromSockaddr4:&sockaddr4];
+ }
+ else
+ {
+ LogWarn(@"Error in getsockname: %@", [self errnoError]);
+ }
+ }
+ else if (socketFamily == AF_INET)
+ {
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
+ {
+ data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len];
+ host = [[self class] hostFromSockaddr6:&sockaddr6];
+ port = [[self class] portFromSockaddr6:&sockaddr6];
+ }
+ else
+ {
+ LogWarn(@"Error in getsockname: %@", [self errnoError]);
+ }
+ }
+
+ if (dataPtr) *dataPtr = data;
+ if (hostPtr) *hostPtr = host;
+ if (portPtr) *portPtr = port;
+
+ return (data != nil);
+}
+
+- (void)maybeUpdateCachedLocalAddress4Info
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) )
+ {
+ return;
+ }
+
+ NSData *address = nil;
+ NSString *host = nil;
+ uint16_t port = 0;
+
+ if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET])
+ {
+
+ cachedLocalAddress4 = address;
+ cachedLocalHost4 = host;
+ cachedLocalPort4 = port;
+ }
+}
+
+- (void)maybeUpdateCachedLocalAddress6Info
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) )
+ {
+ return;
+ }
+
+ NSData *address = nil;
+ NSString *host = nil;
+ uint16_t port = 0;
+
+ if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6])
+ {
+
+ cachedLocalAddress6 = address;
+ cachedLocalHost6 = host;
+ cachedLocalPort6 = port;
+ }
+}
+
+- (NSData *)localAddress
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalAddress4;
+ }
+ else
+ {
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalAddress6;
+ }
+
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSString *)localHost
+{
+ __block NSString *result = nil;
+
+ dispatch_block_t block = ^{
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalHost4;
+ }
+ else
+ {
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalHost6;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (uint16_t)localPort
+{
+ __block uint16_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalPort4;
+ }
+ else
+ {
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalPort6;
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSData *)localAddress_IPv4
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalAddress4;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSString *)localHost_IPv4
+{
+ __block NSString *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalHost4;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (uint16_t)localPort_IPv4
+{
+ __block uint16_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress4Info];
+ result = cachedLocalPort4;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSData *)localAddress_IPv6
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalAddress6;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSString *)localHost_IPv6
+{
+ __block NSString *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalHost6;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (uint16_t)localPort_IPv6
+{
+ __block uint16_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedLocalAddress6Info];
+ result = cachedLocalPort6;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (void)maybeUpdateCachedConnectedAddressInfo
+{
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if (cachedConnectedAddress || (flags & kDidConnect) == 0)
+ {
+ return;
+ }
+
+ NSData *data = nil;
+ NSString *host = nil;
+ uint16_t port = 0;
+ int family = AF_UNSPEC;
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0)
+ {
+ data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len];
+ host = [[self class] hostFromSockaddr4:&sockaddr4];
+ port = [[self class] portFromSockaddr4:&sockaddr4];
+ family = AF_INET;
+ }
+ else
+ {
+ LogWarn(@"Error in getpeername: %@", [self errnoError]);
+ }
+ }
+ else if (socket6FD != SOCKET_NULL)
+ {
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0)
+ {
+ data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len];
+ host = [[self class] hostFromSockaddr6:&sockaddr6];
+ port = [[self class] portFromSockaddr6:&sockaddr6];
+ family = AF_INET6;
+ }
+ else
+ {
+ LogWarn(@"Error in getpeername: %@", [self errnoError]);
+ }
+ }
+
+
+ cachedConnectedAddress = data;
+ cachedConnectedHost = host;
+ cachedConnectedPort = port;
+ cachedConnectedFamily = family;
+}
+
+- (NSData *)connectedAddress
+{
+ __block NSData *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedConnectedAddressInfo];
+ result = cachedConnectedAddress;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (NSString *)connectedHost
+{
+ __block NSString *result = nil;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedConnectedAddressInfo];
+ result = cachedConnectedHost;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (uint16_t)connectedPort
+{
+ __block uint16_t result = 0;
+
+ dispatch_block_t block = ^{
+
+ [self maybeUpdateCachedConnectedAddressInfo];
+ result = cachedConnectedPort;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, AutoreleasedBlock(block));
+
+ return result;
+}
+
+- (BOOL)isConnected
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+ result = (flags & kDidConnect) ? YES : NO;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isClosed
+{
+ __block BOOL result = YES;
+
+ dispatch_block_t block = ^{
+
+ result = (flags & kDidCreateSockets) ? NO : YES;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isIPv4
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+
+ if (flags & kDidCreateSockets)
+ {
+ result = (socket4FD != SOCKET_NULL);
+ }
+ else
+ {
+ result = [self isIPv4Enabled];
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+- (BOOL)isIPv6
+{
+ __block BOOL result = NO;
+
+ dispatch_block_t block = ^{
+
+ if (flags & kDidCreateSockets)
+ {
+ result = (socket6FD != SOCKET_NULL);
+ }
+ else
+ {
+ result = [self isIPv6Enabled];
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Binding
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method runs through the various checks required prior to a bind attempt.
+ * It is shared between the various bind methods.
+**/
+- (BOOL)preBind:(NSError **)errPtr
+{
+ if (![self preOp:errPtr])
+ {
+ return NO;
+ }
+
+ if (flags & kDidBind)
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Cannot bind a socket more than once.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if ((flags & kConnecting) || (flags & kDidConnect))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr
+{
+ return [self bindToPort:port interface:nil error:errPtr];
+}
+
+- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Run through sanity checks
+
+ if (![self preBind:&err])
+ {
+ return_from_block;
+ }
+
+ // Check the given interface
+
+ NSData *interface4 = nil;
+ NSData *interface6 = nil;
+
+ [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6];
+
+ if ((interface4 == nil) && (interface6 == nil))
+ {
+ NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && (interface6 == nil))
+ {
+ NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ if (isIPv6Disabled && (interface4 == nil))
+ {
+ NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Determine protocol(s)
+
+ BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil);
+ BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil);
+
+ // Create the socket(s) if needed
+
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err])
+ {
+ return_from_block;
+ }
+ }
+
+ // Bind the socket(s)
+
+ LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface);
+
+ if (useIPv4)
+ {
+ int status = bind(socket4FD, (struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]);
+ if (status == -1)
+ {
+ [self closeSockets];
+
+ NSString *reason = @"Error in bind() function";
+ err = [self errnoErrorWithReason:reason];
+
+ return_from_block;
+ }
+ }
+
+ if (useIPv6)
+ {
+ int status = bind(socket6FD, (struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]);
+ if (status == -1)
+ {
+ [self closeSockets];
+
+ NSString *reason = @"Error in bind() function";
+ err = [self errnoErrorWithReason:reason];
+
+ return_from_block;
+ }
+ }
+
+ // Update flags
+
+ flags |= kDidBind;
+
+ if (!useIPv4) flags |= kIPv4Deactivated;
+ if (!useIPv6) flags |= kIPv6Deactivated;
+
+ result = YES;
+
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error binding to port/interface: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Run through sanity checks
+
+ if (![self preBind:&err])
+ {
+ return_from_block;
+ }
+
+ // Check the given address
+
+ int addressFamily = [[self class] familyFromAddress:localAddr];
+
+ if (addressFamily == AF_UNSPEC)
+ {
+ NSString *msg = @"A valid IPv4 or IPv6 address was not given";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil;
+ NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil;
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && localAddr4)
+ {
+ NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ if (isIPv6Disabled && localAddr6)
+ {
+ NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Determine protocol(s)
+
+ BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil);
+ BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil);
+
+ // Create the socket(s) if needed
+
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err])
+ {
+ return_from_block;
+ }
+ }
+
+ // Bind the socket(s)
+
+ if (useIPv4)
+ {
+ LogVerbose(@"Binding socket to address(%@:%hu)",
+ [[self class] hostFromAddress:localAddr4],
+ [[self class] portFromAddress:localAddr4]);
+
+ int status = bind(socket4FD, (struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]);
+ if (status == -1)
+ {
+ [self closeSockets];
+
+ NSString *reason = @"Error in bind() function";
+ err = [self errnoErrorWithReason:reason];
+
+ return_from_block;
+ }
+ }
+ else
+ {
+ LogVerbose(@"Binding socket to address(%@:%hu)",
+ [[self class] hostFromAddress:localAddr6],
+ [[self class] portFromAddress:localAddr6]);
+
+ int status = bind(socket6FD, (struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]);
+ if (status == -1)
+ {
+ [self closeSockets];
+
+ NSString *reason = @"Error in bind() function";
+ err = [self errnoErrorWithReason:reason];
+
+ return_from_block;
+ }
+ }
+
+ // Update flags
+
+ flags |= kDidBind;
+
+ if (!useIPv4) flags |= kIPv4Deactivated;
+ if (!useIPv6) flags |= kIPv6Deactivated;
+
+ result = YES;
+
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error binding to address: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Connecting
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * This method runs through the various checks required prior to a connect attempt.
+ * It is shared between the various connect methods.
+**/
+- (BOOL)preConnect:(NSError **)errPtr
+{
+ if (![self preOp:errPtr])
+ {
+ return NO;
+ }
+
+ if ((flags & kConnecting) || (flags & kDidConnect))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Cannot connect a socket more than once.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO;
+ BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO;
+
+ if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Run through sanity checks.
+
+ if (![self preConnect:&err])
+ {
+ return_from_block;
+ }
+
+ // Check parameter(s)
+
+ if (host == nil)
+ {
+ NSString *msg = @"The host param is nil. Should be domain name or IP address string.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Create the socket(s) if needed
+
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ if (![self createSockets:&err])
+ {
+ return_from_block;
+ }
+ }
+
+ // Create special connect packet
+
+ GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init];
+ packet->resolveInProgress = YES;
+
+ // Start asynchronous DNS resolve for host:port on background queue
+
+ LogVerbose(@"Dispatching DNS resolve for connect...");
+
+ [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) {
+
+ // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue,
+ // and immediately returns. Once the async resolve task completes,
+ // this block is executed on our socketQueue.
+
+ packet->resolveInProgress = NO;
+
+ packet->addresses = addresses;
+ packet->error = error;
+
+ [self maybeConnect];
+ }];
+
+ // Updates flags, add connect packet to send queue, and pump send queue
+
+ flags |= kConnecting;
+
+ [sendQueue addObject:packet];
+ [self maybeDequeueSend];
+
+ result = YES;
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error connecting to host/port: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Run through sanity checks.
+
+ if (![self preConnect:&err])
+ {
+ return_from_block;
+ }
+
+ // Check parameter(s)
+
+ if (remoteAddr == nil)
+ {
+ NSString *msg = @"The address param is nil. Should be a valid address.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Create the socket(s) if needed
+
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ if (![self createSockets:&err])
+ {
+ return_from_block;
+ }
+ }
+
+ // The remoteAddr parameter could be of type NSMutableData.
+ // So we copy it to be safe.
+
+ NSData *address = [remoteAddr copy];
+ NSArray *addresses = [NSArray arrayWithObject:address];
+
+ GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init];
+ packet->addresses = addresses;
+
+ // Updates flags, add connect packet to send queue, and pump send queue
+
+ flags |= kConnecting;
+
+ [sendQueue addObject:packet];
+ [self maybeDequeueSend];
+
+ result = YES;
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error connecting to address: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+- (void)maybeConnect
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+
+ BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]];
+
+ if (sendQueueReady)
+ {
+ GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend;
+
+ if (connectPacket->resolveInProgress)
+ {
+ LogVerbose(@"Waiting for DNS resolve...");
+ }
+ else
+ {
+ if (connectPacket->error)
+ {
+ [self notifyDidNotConnect:connectPacket->error];
+ }
+ else
+ {
+ NSData *address = nil;
+ NSError *error = nil;
+
+ int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses];
+
+ // Perform connect
+
+ BOOL result = NO;
+
+ switch (addressFamily)
+ {
+ case AF_INET : result = [self connectWithAddress4:address error:&error]; break;
+ case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break;
+ }
+
+ if (result)
+ {
+ flags |= kDidBind;
+ flags |= kDidConnect;
+
+ cachedConnectedAddress = address;
+ cachedConnectedHost = [[self class] hostFromAddress:address];
+ cachedConnectedPort = [[self class] portFromAddress:address];
+ cachedConnectedFamily = addressFamily;
+
+ [self notifyDidConnectToAddress:address];
+ }
+ else
+ {
+ [self notifyDidNotConnect:error];
+ }
+ }
+
+ flags &= ~kConnecting;
+
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+ }
+ }
+}
+
+- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ int status = connect(socket4FD, (struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]);
+ if (status != 0)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error in connect() function"];
+
+ return NO;
+ }
+
+ [self closeSocket6];
+ flags |= kIPv6Deactivated;
+
+ return YES;
+}
+
+- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ int status = connect(socket6FD, (struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]);
+ if (status != 0)
+ {
+ if (errPtr)
+ *errPtr = [self errnoErrorWithReason:@"Error in connect() function"];
+
+ return NO;
+ }
+
+ [self closeSocket4];
+ flags |= kIPv4Deactivated;
+
+ return YES;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Multicast
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)preJoin:(NSError **)errPtr
+{
+ if (![self preOp:errPtr])
+ {
+ return NO;
+ }
+
+ if (!(flags & kDidBind))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Must bind a socket before joining a multicast group.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ if ((flags & kConnecting) || (flags & kDidConnect))
+ {
+ if (errPtr)
+ {
+ NSString *msg = @"Cannot join a multicast group if connected.";
+ *errPtr = [self badConfigError:msg];
+ }
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr
+{
+ return [self joinMulticastGroup:group onInterface:nil error:errPtr];
+}
+
+- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr
+{
+ // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP
+ return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr];
+}
+
+- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr
+{
+ return [self leaveMulticastGroup:group onInterface:nil error:errPtr];
+}
+
+- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr
+{
+ // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP
+ return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr];
+}
+
+- (BOOL)performMulticastRequest:(int)requestType
+ forGroup:(NSString *)group
+ onInterface:(NSString *)interface
+ error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ // Run through sanity checks
+
+ if (![self preJoin:&err])
+ {
+ return_from_block;
+ }
+
+ // Convert group to address
+
+ NSData *groupAddr4 = nil;
+ NSData *groupAddr6 = nil;
+
+ [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6];
+
+ if ((groupAddr4 == nil) && (groupAddr6 == nil))
+ {
+ NSString *msg = @"Unknown group. Specify valid group IP address.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Convert interface to address
+
+ NSData *interfaceAddr4 = nil;
+ NSData *interfaceAddr6 = nil;
+
+ [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6];
+
+ if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil))
+ {
+ NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address.";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ // Perform join
+
+ if ((socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4)
+ {
+ const struct sockaddr_in *nativeGroup = (struct sockaddr_in *)[groupAddr4 bytes];
+ const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes];
+
+ struct ip_mreq imreq;
+ imreq.imr_multiaddr = nativeGroup->sin_addr;
+ imreq.imr_interface = nativeIface->sin_addr;
+
+ int status = setsockopt(socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq));
+ if (status != 0)
+ {
+ err = [self errnoErrorWithReason:@"Error in setsockopt() function"];
+
+ return_from_block;
+ }
+
+ // Using IPv4 only
+ [self closeSocket6];
+
+ result = YES;
+ }
+ else if ((socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6)
+ {
+ const struct sockaddr_in6 *nativeGroup = (struct sockaddr_in6 *)[groupAddr6 bytes];
+
+ struct ipv6_mreq imreq;
+ imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr;
+ imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6];
+
+ int status = setsockopt(socket6FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq));
+ if (status != 0)
+ {
+ err = [self errnoErrorWithReason:@"Error in setsockopt() function"];
+
+ return_from_block;
+ }
+
+ // Using IPv6 only
+ [self closeSocket4];
+
+ result = YES;
+ }
+ else
+ {
+ NSString *msg = @"Socket, group, and interface do not have matching IP versions";
+ err = [self badParamError:msg];
+
+ return_from_block;
+ }
+
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Broadcast
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr
+{
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ if (![self preOp:&err])
+ {
+ return_from_block;
+ }
+
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ if (![self createSockets:&err])
+ {
+ return_from_block;
+ }
+ }
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ int value = flag ? 1 : 0;
+ int error = setsockopt(socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value));
+
+ if (error)
+ {
+ err = [self errnoErrorWithReason:@"Error in setsockopt() function"];
+
+ return_from_block;
+ }
+ result = YES;
+ }
+
+ // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link.
+ // The same effect can be achieved by sending a packet to the link-local all hosts multicast group.
+
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Sending
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)sendData:(NSData *)data withTag:(long)tag
+{
+ [self sendData:data withTimeout:-1.0 tag:tag];
+}
+
+- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ LogTrace();
+
+ if ([data length] == 0)
+ {
+ LogWarn(@"Ignoring attempt to send nil/empty data.");
+ return;
+ }
+
+ GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [sendQueue addObject:packet];
+ [self maybeDequeueSend];
+ }});
+
+}
+
+- (void)sendData:(NSData *)data
+ toHost:(NSString *)host
+ port:(uint16_t)port
+ withTimeout:(NSTimeInterval)timeout
+ tag:(long)tag
+{
+ LogTrace();
+
+ if ([data length] == 0)
+ {
+ LogWarn(@"Ignoring attempt to send nil/empty data.");
+ return;
+ }
+
+ GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag];
+ packet->resolveInProgress = YES;
+
+ [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) {
+
+ // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue,
+ // and immediately returns. Once the async resolve task completes,
+ // this block is executed on our socketQueue.
+
+ packet->resolveInProgress = NO;
+
+ packet->resolvedAddresses = addresses;
+ packet->resolveError = error;
+
+ if (packet == currentSend)
+ {
+ LogVerbose(@"currentSend - address resolved");
+ [self doPreSend];
+ }
+ }];
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [sendQueue addObject:packet];
+ [self maybeDequeueSend];
+
+ }});
+
+}
+
+- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag
+{
+ LogTrace();
+
+ if ([data length] == 0)
+ {
+ LogWarn(@"Ignoring attempt to send nil/empty data.");
+ return;
+ }
+
+ GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag];
+ packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr];
+ packet->address = remoteAddr;
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [sendQueue addObject:packet];
+ [self maybeDequeueSend];
+ }});
+}
+
+- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue
+{
+ [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES];
+}
+
+- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock
+ withQueue:(dispatch_queue_t)filterQueue
+ isAsynchronous:(BOOL)isAsynchronous
+{
+ GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL;
+ dispatch_queue_t newFilterQueue = NULL;
+
+ if (filterBlock)
+ {
+ NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block.");
+
+ newFilterBlock = [filterBlock copy];
+ newFilterQueue = filterQueue;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_retain(newFilterQueue);
+ #endif
+ }
+
+ dispatch_block_t block = ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (sendFilterQueue) dispatch_release(sendFilterQueue);
+ #endif
+
+ sendFilterBlock = newFilterBlock;
+ sendFilterQueue = newFilterQueue;
+ sendFilterAsync = isAsynchronous;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (void)maybeDequeueSend
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ // If we don't have a send operation already in progress
+ if (currentSend == nil)
+ {
+ // Create the sockets if needed
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ NSError *err = nil;
+ if (![self createSockets:&err])
+ {
+ [self closeWithError:err];
+ return;
+ }
+ }
+
+ while ([sendQueue count] > 0)
+ {
+ // Dequeue the next object in the queue
+ currentSend = [sendQueue objectAtIndex:0];
+ [sendQueue removeObjectAtIndex:0];
+
+ if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]])
+ {
+ [self maybeConnect];
+
+ return; // The maybeConnect method, if it connects, will invoke this method again
+ }
+ else if (currentSend->resolveError)
+ {
+ // Notify delegate
+ [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError];
+
+ // Clear currentSend
+ currentSend = nil;
+
+ continue;
+ }
+ else
+ {
+ // Start preprocessing checks on the send packet
+ [self doPreSend];
+
+ break;
+ }
+ }
+
+ if ((currentSend == nil) && (flags & kCloseAfterSends))
+ {
+ [self closeWithError:nil];
+ }
+ }
+}
+
+/**
+ * This method is called after a sendPacket has been dequeued.
+ * It performs various preprocessing checks on the packet,
+ * and queries the sendFilter (if set) to determine if the packet can be sent.
+ *
+ * If the packet passes all checks, it will be passed on to the doSend method.
+**/
+- (void)doPreSend
+{
+ LogTrace();
+
+ //
+ // 1. Check for problems with send packet
+ //
+
+ BOOL waitingForResolve = NO;
+ NSError *error = nil;
+
+ if (flags & kDidConnect)
+ {
+ // Connected socket
+
+ if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError)
+ {
+ NSString *msg = @"Cannot specify destination of packet for connected socket";
+ error = [self badConfigError:msg];
+ }
+ else
+ {
+ currentSend->address = cachedConnectedAddress;
+ currentSend->addressFamily = cachedConnectedFamily;
+ }
+ }
+ else
+ {
+ // Non-Connected socket
+
+ if (currentSend->resolveInProgress)
+ {
+ // We're waiting for the packet's destination to be resolved.
+ waitingForResolve = YES;
+ }
+ else if (currentSend->resolveError)
+ {
+ error = currentSend->resolveError;
+ }
+ else if (currentSend->address == nil)
+ {
+ if (currentSend->resolvedAddresses == nil)
+ {
+ NSString *msg = @"You must specify destination of packet for a non-connected socket";
+ error = [self badConfigError:msg];
+ }
+ else
+ {
+ // Pick the proper address to use (out of possibly several resolved addresses)
+
+ NSData *address = nil;
+ int addressFamily = AF_UNSPEC;
+
+ addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses];
+
+ currentSend->address = address;
+ currentSend->addressFamily = addressFamily;
+ }
+ }
+ }
+
+ if (waitingForResolve)
+ {
+ // We're waiting for the packet's destination to be resolved.
+
+ LogVerbose(@"currentSend - waiting for address resolve");
+
+ if (flags & kSock4CanAcceptBytes) {
+ [self suspendSend4Source];
+ }
+ if (flags & kSock6CanAcceptBytes) {
+ [self suspendSend6Source];
+ }
+
+ return;
+ }
+
+ if (error)
+ {
+ // Unable to send packet due to some error.
+ // Notify delegate and move on.
+
+ [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error];
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+
+ return;
+ }
+
+ //
+ // 2. Query sendFilter (if applicable)
+ //
+
+ if (sendFilterBlock && sendFilterQueue)
+ {
+ // Query sendFilter
+
+ if (sendFilterAsync)
+ {
+ // Scenario 1 of 3 - Need to asynchronously query sendFilter
+
+ currentSend->filterInProgress = YES;
+ GCDAsyncUdpSendPacket *sendPacket = currentSend;
+
+ dispatch_async(sendFilterQueue, ^{ @autoreleasepool {
+
+ BOOL allowed = sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag);
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ sendPacket->filterInProgress = NO;
+ if (sendPacket == currentSend)
+ {
+ if (allowed)
+ {
+ [self doSend];
+ }
+ else
+ {
+ LogVerbose(@"currentSend - silently dropped by sendFilter");
+
+ [self notifyDidSendDataWithTag:currentSend->tag];
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+ }
+ }
+ }});
+ }});
+ }
+ else
+ {
+ // Scenario 2 of 3 - Need to synchronously query sendFilter
+
+ __block BOOL allowed = YES;
+
+ dispatch_sync(sendFilterQueue, ^{ @autoreleasepool {
+
+ allowed = sendFilterBlock(currentSend->buffer, currentSend->address, currentSend->tag);
+ }});
+
+ if (allowed)
+ {
+ [self doSend];
+ }
+ else
+ {
+ LogVerbose(@"currentSend - silently dropped by sendFilter");
+
+ [self notifyDidSendDataWithTag:currentSend->tag];
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+ }
+ }
+ }
+ else // if (!sendFilterBlock || !sendFilterQueue)
+ {
+ // Scenario 3 of 3 - No sendFilter. Just go straight into sending.
+
+ [self doSend];
+ }
+}
+
+/**
+ * This method performs the actual sending of data in the currentSend packet.
+ * It should only be called if the
+**/
+- (void)doSend
+{
+ LogTrace();
+
+ NSAssert(currentSend != nil, @"Invalid logic");
+
+ // Perform the actual send
+
+ ssize_t result = 0;
+
+ if (flags & kDidConnect)
+ {
+ // Connected socket
+
+ const void *buffer = [currentSend->buffer bytes];
+ size_t length = (size_t)[currentSend->buffer length];
+
+ if (currentSend->addressFamily == AF_INET)
+ {
+ result = send(socket4FD, buffer, length, 0);
+ LogVerbose(@"send(socket4FD) = %d", result);
+ }
+ else
+ {
+ result = send(socket6FD, buffer, length, 0);
+ LogVerbose(@"send(socket6FD) = %d", result);
+ }
+ }
+ else
+ {
+ // Non-Connected socket
+
+ const void *buffer = [currentSend->buffer bytes];
+ size_t length = (size_t)[currentSend->buffer length];
+
+ const void *dst = [currentSend->address bytes];
+ socklen_t dstSize = (socklen_t)[currentSend->address length];
+
+ if (currentSend->addressFamily == AF_INET)
+ {
+ result = sendto(socket4FD, buffer, length, 0, dst, dstSize);
+ LogVerbose(@"sendto(socket4FD) = %d", result);
+ }
+ else
+ {
+ result = sendto(socket6FD, buffer, length, 0, dst, dstSize);
+ LogVerbose(@"sendto(socket6FD) = %d", result);
+ }
+ }
+
+ // If the socket wasn't bound before, it is now
+
+ if ((flags & kDidBind) == 0)
+ {
+ flags |= kDidBind;
+ }
+
+ // Check the results.
+ //
+ // From the send() & sendto() manpage:
+ //
+ // Upon successful completion, the number of bytes which were sent is returned.
+ // Otherwise, -1 is returned and the global variable errno is set to indicate the error.
+
+ BOOL waitingForSocket = NO;
+ NSError *socketError = nil;
+
+ if (result == 0)
+ {
+ waitingForSocket = YES;
+ }
+ else if (result < 0)
+ {
+ if (errno == EAGAIN)
+ waitingForSocket = YES;
+ else
+ socketError = [self errnoErrorWithReason:@"Error in send() function."];
+ }
+
+ if (waitingForSocket)
+ {
+ // Not enough room in the underlying OS socket send buffer.
+ // Wait for a notification of available space.
+
+ LogVerbose(@"currentSend - waiting for socket");
+
+ if (!(flags & kSock4CanAcceptBytes)) {
+ [self resumeSend4Source];
+ }
+ if (!(flags & kSock6CanAcceptBytes)) {
+ [self resumeSend6Source];
+ }
+
+ if ((sendTimer == NULL) && (currentSend->timeout >= 0.0))
+ {
+ // Unable to send packet right away.
+ // Start timer to timeout the send operation.
+
+ [self setupSendTimerWithTimeout:currentSend->timeout];
+ }
+ }
+ else if (socketError)
+ {
+ [self closeWithError:socketError];
+ }
+ else // done
+ {
+ [self notifyDidSendDataWithTag:currentSend->tag];
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+ }
+}
+
+/**
+ * Releases all resources associated with the currentSend.
+**/
+- (void)endCurrentSend
+{
+ if (sendTimer)
+ {
+ dispatch_source_cancel(sendTimer);
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_release(sendTimer);
+ #endif
+ sendTimer = NULL;
+ }
+
+ currentSend = nil;
+}
+
+/**
+ * Performs the operations to timeout the current send operation, and move on.
+**/
+- (void)doSendTimeout
+{
+ LogTrace();
+
+ [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]];
+ [self endCurrentSend];
+ [self maybeDequeueSend];
+}
+
+/**
+ * Sets up a timer that fires to timeout the current send operation.
+ * This method should only be called once per send packet.
+**/
+- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout
+{
+ NSAssert(sendTimer == NULL, @"Invalid logic");
+ NSAssert(timeout >= 0.0, @"Invalid logic");
+
+ LogTrace();
+
+ sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);
+
+ dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool {
+
+ [self doSendTimeout];
+ }});
+
+ dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
+
+ dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0);
+ dispatch_resume(sendTimer);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Receiving
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (BOOL)receiveOnce:(NSError **)errPtr
+{
+ LogTrace();
+
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{
+
+ if ((flags & kReceiveOnce) == 0)
+ {
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ NSString *msg = @"Must bind socket before you can receive data. "
+ @"You can do this explicitly via bind, or implicitly via connect or by sending data.";
+
+ err = [self badConfigError:msg];
+ return_from_block;
+ }
+
+ flags |= kReceiveOnce; // Enable
+ flags &= ~kReceiveContinuous; // Disable
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self doReceive];
+ }});
+ }
+
+ result = YES;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error in beginReceiving: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+- (BOOL)beginReceiving:(NSError **)errPtr
+{
+ LogTrace();
+
+ __block BOOL result = NO;
+ __block NSError *err = nil;
+
+ dispatch_block_t block = ^{
+
+ if ((flags & kReceiveContinuous) == 0)
+ {
+ if ((flags & kDidCreateSockets) == 0)
+ {
+ NSString *msg = @"Must bind socket before you can receive data. "
+ @"You can do this explicitly via bind, or implicitly via connect or by sending data.";
+
+ err = [self badConfigError:msg];
+ return_from_block;
+ }
+
+ flags |= kReceiveContinuous; // Enable
+ flags &= ~kReceiveOnce; // Disable
+
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ [self doReceive];
+ }});
+ }
+
+ result = YES;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+
+ if (err)
+ LogError(@"Error in beginReceiving: %@", err);
+
+ if (errPtr)
+ *errPtr = err;
+
+ return result;
+}
+
+- (void)pauseReceiving
+{
+ LogTrace();
+
+ dispatch_block_t block = ^{
+
+ flags &= ~kReceiveOnce; // Disable
+ flags &= ~kReceiveContinuous; // Disable
+
+ if (socket4FDBytesAvailable > 0) {
+ [self suspendReceive4Source];
+ }
+ if (socket6FDBytesAvailable > 0) {
+ [self suspendReceive6Source];
+ }
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue
+{
+ [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES];
+}
+
+- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock
+ withQueue:(dispatch_queue_t)filterQueue
+ isAsynchronous:(BOOL)isAsynchronous
+{
+ GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL;
+ dispatch_queue_t newFilterQueue = NULL;
+
+ if (filterBlock)
+ {
+ NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block.");
+
+ newFilterBlock = [filterBlock copy];
+ newFilterQueue = filterQueue;
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ dispatch_retain(newFilterQueue);
+ #endif
+ }
+
+ dispatch_block_t block = ^{
+
+ #if NEEDS_DISPATCH_RETAIN_RELEASE
+ if (receiveFilterQueue) dispatch_release(receiveFilterQueue);
+ #endif
+
+ receiveFilterBlock = newFilterBlock;
+ receiveFilterQueue = newFilterQueue;
+ receiveFilterAsync = isAsynchronous;
+ };
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+- (void)doReceive
+{
+ LogTrace();
+
+ if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0)
+ {
+ LogVerbose(@"Receiving is paused...");
+
+ if (socket4FDBytesAvailable > 0) {
+ [self suspendReceive4Source];
+ }
+ if (socket6FDBytesAvailable > 0) {
+ [self suspendReceive6Source];
+ }
+
+ return;
+ }
+
+ if ((flags & kReceiveOnce) && (pendingFilterOperations > 0))
+ {
+ LogVerbose(@"Receiving is temporarily paused (pending filter operations)...");
+
+ if (socket4FDBytesAvailable > 0) {
+ [self suspendReceive4Source];
+ }
+ if (socket6FDBytesAvailable > 0) {
+ [self suspendReceive6Source];
+ }
+
+ return;
+ }
+
+ if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0))
+ {
+ LogVerbose(@"No data available to receive...");
+
+ if (socket4FDBytesAvailable == 0) {
+ [self resumeReceive4Source];
+ }
+ if (socket6FDBytesAvailable == 0) {
+ [self resumeReceive6Source];
+ }
+
+ return;
+ }
+
+ // Figure out if we should receive on socket4 or socket6
+
+ BOOL doReceive4;
+
+ if (flags & kDidConnect)
+ {
+ // Connected socket
+
+ doReceive4 = (socket4FD != SOCKET_NULL);
+ }
+ else
+ {
+ // Non-Connected socket
+
+ if (socket4FDBytesAvailable > 0)
+ {
+ if (socket6FDBytesAvailable > 0)
+ {
+ // Bytes available on socket4 & socket6
+
+ doReceive4 = (flags & kFlipFlop) ? YES : NO;
+
+ flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit)
+ }
+ else {
+ // Bytes available on socket4, but not socket6
+ doReceive4 = YES;
+ }
+ }
+ else {
+ // Bytes available on socket6, but not socket4
+ doReceive4 = NO;
+ }
+ }
+
+ // Perform socket IO
+
+ ssize_t result = 0;
+
+ NSData *data = nil;
+ NSData *addr4 = nil;
+ NSData *addr6 = nil;
+
+ if (doReceive4)
+ {
+ NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic");
+ LogVerbose(@"Receiving on IPv4");
+
+ struct sockaddr_in sockaddr4;
+ socklen_t sockaddr4len = sizeof(sockaddr4);
+
+ size_t bufSize = MIN(max4ReceiveSize, socket4FDBytesAvailable);
+ void *buf = malloc(bufSize);
+
+ result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len);
+ LogVerbose(@"recvfrom(socket4FD) = %i", (int)result);
+
+ if (result > 0)
+ {
+ if ((size_t)result >= socket4FDBytesAvailable)
+ socket4FDBytesAvailable = 0;
+ else
+ socket4FDBytesAvailable -= result;
+
+ if ((size_t)result != bufSize) {
+ buf = realloc(buf, result);
+ }
+
+ data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES];
+ addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len];
+ }
+ else
+ {
+ LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]);
+ socket4FDBytesAvailable = 0;
+ free(buf);
+ }
+ }
+ else
+ {
+ NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic");
+ LogVerbose(@"Receiving on IPv6");
+
+ struct sockaddr_in6 sockaddr6;
+ socklen_t sockaddr6len = sizeof(sockaddr6);
+
+ size_t bufSize = MIN(max6ReceiveSize, socket6FDBytesAvailable);
+ void *buf = malloc(bufSize);
+
+ result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len);
+ LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result);
+
+ if (result > 0)
+ {
+ if ((size_t)result >= socket6FDBytesAvailable)
+ socket6FDBytesAvailable = 0;
+ else
+ socket6FDBytesAvailable -= result;
+
+ if ((size_t)result != bufSize) {
+ buf = realloc(buf, result);
+ }
+
+ data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES];
+ addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len];
+ }
+ else
+ {
+ LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]);
+ socket6FDBytesAvailable = 0;
+ free(buf);
+ }
+ }
+
+
+ BOOL waitingForSocket = NO;
+ BOOL notifiedDelegate = NO;
+ BOOL ignored = NO;
+
+ NSError *socketError = nil;
+
+ if (result == 0)
+ {
+ waitingForSocket = YES;
+ }
+ else if (result < 0)
+ {
+ if (errno == EAGAIN)
+ waitingForSocket = YES;
+ else
+ socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"];
+ }
+ else
+ {
+ if (flags & kDidConnect)
+ {
+ if (addr4 && ![self isConnectedToAddress4:addr4])
+ ignored = YES;
+ if (addr6 && ![self isConnectedToAddress6:addr6])
+ ignored = YES;
+ }
+
+ NSData *addr = (addr4 != nil) ? addr4 : addr6;
+
+ if (!ignored)
+ {
+ if (receiveFilterBlock && receiveFilterQueue)
+ {
+ // Run data through filter, and if approved, notify delegate
+
+ __block id filterContext = nil;
+ __block BOOL allowed = NO;
+
+ if (receiveFilterAsync)
+ {
+ pendingFilterOperations++;
+ dispatch_async(receiveFilterQueue, ^{ @autoreleasepool {
+
+ allowed = receiveFilterBlock(data, addr, &filterContext);
+
+ // Transition back to socketQueue to get the current delegate / delegateQueue
+ dispatch_async(socketQueue, ^{ @autoreleasepool {
+
+ pendingFilterOperations--;
+
+ if (allowed)
+ {
+ [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext];
+ }
+ else
+ {
+ LogVerbose(@"received packet silently dropped by receiveFilter");
+ }
+
+ if (flags & kReceiveOnce)
+ {
+ if (allowed)
+ {
+ // The delegate has been notified,
+ // so our receive once operation has completed.
+ flags &= ~kReceiveOnce;
+ }
+ else if (pendingFilterOperations == 0)
+ {
+ // All pending filter operations have completed,
+ // and none were allowed through.
+ // Our receive once operation hasn't completed yet.
+ [self doReceive];
+ }
+ }
+ }});
+ }});
+ }
+ else // if (!receiveFilterAsync)
+ {
+ dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool {
+
+ allowed = receiveFilterBlock(data, addr, &filterContext);
+ }});
+
+ if (allowed)
+ {
+ [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext];
+ notifiedDelegate = YES;
+ }
+ else
+ {
+ LogVerbose(@"received packet silently dropped by receiveFilter");
+ ignored = YES;
+ }
+ }
+ }
+ else // if (!receiveFilterBlock || !receiveFilterQueue)
+ {
+ [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil];
+ notifiedDelegate = YES;
+ }
+ }
+ }
+
+ if (waitingForSocket)
+ {
+ // Wait for a notification of available data.
+
+ if (socket4FDBytesAvailable == 0) {
+ [self resumeReceive4Source];
+ }
+ if (socket6FDBytesAvailable == 0) {
+ [self resumeReceive6Source];
+ }
+ }
+ else if (socketError)
+ {
+ [self closeWithError:socketError];
+ }
+ else
+ {
+ if (flags & kReceiveContinuous)
+ {
+ // Continuous receive mode
+ [self doReceive];
+ }
+ else
+ {
+ // One-at-a-time receive mode
+ if (notifiedDelegate)
+ {
+ // The delegate has been notified (no set filter).
+ // So our receive once operation has completed.
+ flags &= ~kReceiveOnce;
+ }
+ else if (ignored)
+ {
+ [self doReceive];
+ }
+ else
+ {
+ // Waiting on asynchronous receive filter...
+ }
+ }
+ }
+}
+
+- (void)doReceiveEOF
+{
+ LogTrace();
+
+ [self closeWithError:[self socketClosedError]];
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Closing
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+- (void)closeWithError:(NSError *)error
+{
+ LogVerbose(@"closeWithError: %@", error);
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if (currentSend) [self endCurrentSend];
+
+ [sendQueue removeAllObjects];
+
+ // If a socket has been created, we should notify the delegate.
+ BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO;
+
+ // Close all sockets, send/receive sources, cfstreams, etc
+#if TARGET_OS_IPHONE
+ [self removeStreamsFromRunLoop];
+ [self closeReadAndWriteStreams];
+#endif
+ [self closeSockets];
+
+ // Clear all flags (config remains as is)
+ flags = 0;
+
+ if (shouldCallDelegate)
+ {
+ [self notifyDidCloseWithError:error];
+ }
+}
+
+- (void)close
+{
+ LogTrace();
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ [self closeWithError:nil];
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+}
+
+- (void)closeAfterSending
+{
+ LogTrace();
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ flags |= kCloseAfterSends;
+
+ if (currentSend == nil && [sendQueue count] == 0)
+ {
+ [self closeWithError:nil];
+ }
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark CFStream
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if TARGET_OS_IPHONE
+
+static NSThread *listenerThread;
+
++ (void)ignore:(id)_
+{}
+
++ (void)startListenerThreadIfNeeded
+{
+ static dispatch_once_t predicate;
+ dispatch_once(&predicate, ^{
+
+ listenerThread = [[NSThread alloc] initWithTarget:self
+ selector:@selector(listenerThread)
+ object:nil];
+ [listenerThread start];
+ });
+}
+
++ (void)listenerThread
+{
+ @autoreleasepool {
+
+ [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName];
+
+ LogInfo(@"ListenerThread: Started");
+
+ // We can't run the run loop unless it has an associated input source or a timer.
+ // So we'll just create a timer that will never fire - unless the server runs for a decades.
+ [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
+ target:self
+ selector:@selector(ignore:)
+ userInfo:nil
+ repeats:YES];
+
+ [[NSRunLoop currentRunLoop] run];
+
+ LogInfo(@"ListenerThread: Stopped");
+ }
+}
+
++ (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket
+{
+ LogTrace();
+ NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread");
+
+ CFRunLoopRef runLoop = CFRunLoopGetCurrent();
+
+ if (asyncUdpSocket->readStream4)
+ CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->readStream6)
+ CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->writeStream4)
+ CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->writeStream6)
+ CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode);
+}
+
++ (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket
+{
+ LogTrace();
+ NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread");
+
+ CFRunLoopRef runLoop = CFRunLoopGetCurrent();
+
+ if (asyncUdpSocket->readStream4)
+ CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->readStream6)
+ CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->writeStream4)
+ CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode);
+
+ if (asyncUdpSocket->writeStream6)
+ CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode);
+}
+
+static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
+{
+ @autoreleasepool {
+ GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo;
+
+ switch(type)
+ {
+ case kCFStreamEventOpenCompleted:
+ {
+ LogCVerbose(@"CFReadStreamCallback - Open");
+ break;
+ }
+ case kCFStreamEventHasBytesAvailable:
+ {
+ LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable");
+ break;
+ }
+ case kCFStreamEventErrorOccurred:
+ case kCFStreamEventEndEncountered:
+ {
+ NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream);
+ if (error == nil && type == kCFStreamEventEndEncountered)
+ {
+ error = [asyncUdpSocket socketClosedError];
+ }
+
+ dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFReadStreamCallback - %@",
+ (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered");
+
+ if (stream != asyncUdpSocket->readStream4 &&
+ stream != asyncUdpSocket->readStream6 )
+ {
+ LogCVerbose(@"CFReadStreamCallback - Ignored");
+ return_from_block;
+ }
+
+ [asyncUdpSocket closeWithError:error];
+
+ }});
+
+ break;
+ }
+ default:
+ {
+ LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type);
+ }
+ }
+ }
+}
+
+static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)
+{
+ @autoreleasepool {
+ GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo;
+
+ switch(type)
+ {
+ case kCFStreamEventOpenCompleted:
+ {
+ LogCVerbose(@"CFWriteStreamCallback - Open");
+ break;
+ }
+ case kCFStreamEventCanAcceptBytes:
+ {
+ LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes");
+ break;
+ }
+ case kCFStreamEventErrorOccurred:
+ case kCFStreamEventEndEncountered:
+ {
+ NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream);
+ if (error == nil && type == kCFStreamEventEndEncountered)
+ {
+ error = [asyncUdpSocket socketClosedError];
+ }
+
+ dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool {
+
+ LogCVerbose(@"CFWriteStreamCallback - %@",
+ (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered");
+
+ if (stream != asyncUdpSocket->writeStream4 &&
+ stream != asyncUdpSocket->writeStream6 )
+ {
+ LogCVerbose(@"CFWriteStreamCallback - Ignored");
+ return_from_block;
+ }
+
+ [asyncUdpSocket closeWithError:error];
+
+ }});
+
+ break;
+ }
+ default:
+ {
+ LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type);
+ }
+ }
+ }
+}
+
+- (BOOL)createReadAndWriteStreams:(NSError **)errPtr
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ NSError *err = nil;
+
+ if (readStream4 || writeStream4 || readStream6 || writeStream6)
+ {
+ // Streams already created
+ return YES;
+ }
+
+ if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL)
+ {
+ err = [self otherError:@"Cannot create streams without a file descriptor"];
+ goto Failed;
+ }
+
+ // Create streams
+
+ LogVerbose(@"Creating read and write stream(s)...");
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4);
+ if (!readStream4 || !writeStream4)
+ {
+ err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"];
+ goto Failed;
+ }
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6);
+ if (!readStream6 || !writeStream6)
+ {
+ err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"];
+ goto Failed;
+ }
+ }
+
+ // Ensure the CFStream's don't close our underlying socket
+
+ CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+ CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+
+ CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+ CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse);
+
+ return YES;
+
+Failed:
+ if (readStream4)
+ {
+ CFReadStreamClose(readStream4);
+ CFRelease(readStream4);
+ readStream4 = NULL;
+ }
+ if (writeStream4)
+ {
+ CFWriteStreamClose(writeStream4);
+ CFRelease(writeStream4);
+ writeStream4 = NULL;
+ }
+ if (readStream6)
+ {
+ CFReadStreamClose(readStream6);
+ CFRelease(readStream6);
+ readStream6 = NULL;
+ }
+ if (writeStream6)
+ {
+ CFWriteStreamClose(writeStream6);
+ CFRelease(writeStream6);
+ writeStream6 = NULL;
+ }
+
+ if (errPtr)
+ *errPtr = err;
+
+ return NO;
+}
+
+- (BOOL)registerForStreamCallbacks:(NSError **)errPtr
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null");
+
+ NSError *err = nil;
+
+ streamContext.version = 0;
+ streamContext.info = (__bridge void *)self;
+ streamContext.retain = nil;
+ streamContext.release = nil;
+ streamContext.copyDescription = nil;
+
+ CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
+ CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered;
+
+// readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable);
+// writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes);
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ if (readStream4 == NULL || writeStream4 == NULL)
+ {
+ err = [self otherError:@"Read/Write stream4 is null"];
+ goto Failed;
+ }
+
+ BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext);
+ BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext);
+
+ if (!r1 || !r2)
+ {
+ err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"];
+ goto Failed;
+ }
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ if (readStream6 == NULL || writeStream6 == NULL)
+ {
+ err = [self otherError:@"Read/Write stream6 is null"];
+ goto Failed;
+ }
+
+ BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext);
+ BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext);
+
+ if (!r1 || !r2)
+ {
+ err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"];
+ goto Failed;
+ }
+ }
+
+ return YES;
+
+Failed:
+ if (readStream4) {
+ CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL);
+ }
+ if (writeStream4) {
+ CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL);
+ }
+ if (readStream6) {
+ CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL);
+ }
+ if (writeStream6) {
+ CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL);
+ }
+
+ if (errPtr) *errPtr = err;
+ return NO;
+}
+
+- (BOOL)addStreamsToRunLoop:(NSError **)errPtr
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null");
+
+ if (!(flags & kAddedStreamListener))
+ {
+ [[self class] startListenerThreadIfNeeded];
+ [[self class] performSelector:@selector(addStreamListener:)
+ onThread:listenerThread
+ withObject:self
+ waitUntilDone:YES];
+
+ flags |= kAddedStreamListener;
+ }
+
+ return YES;
+}
+
+- (BOOL)openStreams:(NSError **)errPtr
+{
+ LogTrace();
+
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+ NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null");
+
+ NSError *err = nil;
+
+ if (socket4FD != SOCKET_NULL)
+ {
+ BOOL r1 = CFReadStreamOpen(readStream4);
+ BOOL r2 = CFWriteStreamOpen(writeStream4);
+
+ if (!r1 || !r2)
+ {
+ err = [self otherError:@"Error in CFStreamOpen() [IPv4]"];
+ goto Failed;
+ }
+ }
+
+ if (socket6FD != SOCKET_NULL)
+ {
+ BOOL r1 = CFReadStreamOpen(readStream6);
+ BOOL r2 = CFWriteStreamOpen(writeStream6);
+
+ if (!r1 || !r2)
+ {
+ err = [self otherError:@"Error in CFStreamOpen() [IPv6]"];
+ goto Failed;
+ }
+ }
+
+ return YES;
+
+Failed:
+ if (errPtr) *errPtr = err;
+ return NO;
+}
+
+- (void)removeStreamsFromRunLoop
+{
+ LogTrace();
+ NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");
+
+ if (flags & kAddedStreamListener)
+ {
+ [[self class] performSelector:@selector(removeStreamListener:)
+ onThread:listenerThread
+ withObject:self
+ waitUntilDone:YES];
+
+ flags &= ~kAddedStreamListener;
+ }
+}
+
+- (void)closeReadAndWriteStreams
+{
+ LogTrace();
+
+ if (readStream4)
+ {
+ CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL);
+ CFReadStreamClose(readStream4);
+ CFRelease(readStream4);
+ readStream4 = NULL;
+ }
+ if (writeStream4)
+ {
+ CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL);
+ CFWriteStreamClose(writeStream4);
+ CFRelease(writeStream4);
+ writeStream4 = NULL;
+ }
+ if (readStream6)
+ {
+ CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL);
+ CFReadStreamClose(readStream6);
+ CFRelease(readStream6);
+ readStream6 = NULL;
+ }
+ if (writeStream6)
+ {
+ CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL);
+ CFWriteStreamClose(writeStream6);
+ CFRelease(writeStream6);
+ writeStream6 = NULL;
+ }
+}
+
+#endif
+
+- (void)applicationWillEnterForeground:(NSNotification *)notification
+{
+ LogTrace();
+
+ // If the application was backgrounded, then iOS may have shut down our sockets.
+ // So we take a quick look to see if any of them received an EOF.
+
+ dispatch_block_t block = ^{ @autoreleasepool {
+
+ [self resumeReceive4Source];
+ [self resumeReceive6Source];
+ }};
+
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_async(socketQueue, block);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Advanced
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * See header file for big discussion of this method.
+ **/
+- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue
+{
+ void *nonNullUnusedPointer = (__bridge void *)self;
+ dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL);
+}
+
+/**
+ * See header file for big discussion of this method.
+ **/
+- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue
+{
+ dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL);
+}
+
+- (void)performBlock:(dispatch_block_t)block
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ block();
+ else
+ dispatch_sync(socketQueue, block);
+}
+
+- (int)socketFD
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ if (socket4FD != SOCKET_NULL)
+ return socket4FD;
+ else
+ return socket6FD;
+}
+
+- (int)socket4FD
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ return socket4FD;
+}
+
+- (int)socket6FD
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return SOCKET_NULL;
+ }
+
+ return socket6FD;
+}
+
+#if TARGET_OS_IPHONE
+
+- (CFReadStreamRef)readStream
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return NULL;
+ }
+
+ NSError *err = nil;
+ if (![self createReadAndWriteStreams:&err])
+ {
+ LogError(@"Error creating CFStream(s): %@", err);
+ return NULL;
+ }
+
+ // Todo...
+
+ if (readStream4)
+ return readStream4;
+ else
+ return readStream6;
+}
+
+- (CFWriteStreamRef)writeStream
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return NULL;
+ }
+
+ NSError *err = nil;
+ if (![self createReadAndWriteStreams:&err])
+ {
+ LogError(@"Error creating CFStream(s): %@", err);
+ return NULL;
+ }
+
+ if (writeStream4)
+ return writeStream4;
+ else
+ return writeStream6;
+}
+
+- (BOOL)enableBackgroundingOnSockets
+{
+ if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey))
+ {
+ LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation",
+ THIS_FILE, THIS_METHOD);
+ return NO;
+ }
+
+ // Why is this commented out?
+ // See comments below.
+
+// NSError *err = nil;
+// if (![self createReadAndWriteStreams:&err])
+// {
+// LogError(@"Error creating CFStream(s): %@", err);
+// return NO;
+// }
+//
+// LogVerbose(@"Enabling backgrouding on socket");
+//
+// BOOL r1, r2;
+//
+// if (readStream4 && writeStream4)
+// {
+// r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+// r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+//
+// if (!r1 || !r2)
+// {
+// LogError(@"Error setting voip type (IPv4)");
+// return NO;
+// }
+// }
+//
+// if (readStream6 && writeStream6)
+// {
+// r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+// r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP);
+//
+// if (!r1 || !r2)
+// {
+// LogError(@"Error setting voip type (IPv6)");
+// return NO;
+// }
+// }
+//
+// return YES;
+
+ // The above code will actually appear to work.
+ // The methods will return YES, and everything will appear fine.
+ //
+ // One tiny problem: the sockets will still get closed when the app gets backgrounded.
+ //
+ // Apple does not officially support backgrounding UDP sockets.
+
+ return NO;
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark Class Methods
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
+{
+ char addrBuf[INET_ADDRSTRLEN];
+
+ if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
+ {
+ addrBuf[0] = '\0';
+ }
+
+ return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
+}
+
++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
+{
+ char addrBuf[INET6_ADDRSTRLEN];
+
+ if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL)
+ {
+ addrBuf[0] = '\0';
+ }
+
+ return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding];
+}
+
++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4
+{
+ return ntohs(pSockaddr4->sin_port);
+}
+
++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6
+{
+ return ntohs(pSockaddr6->sin6_port);
+}
+
++ (NSString *)hostFromAddress:(NSData *)address
+{
+ NSString *host = nil;
+ [self getHost:&host port:NULL family:NULL fromAddress:address];
+
+ return host;
+}
+
++ (uint16_t)portFromAddress:(NSData *)address
+{
+ uint16_t port = 0;
+ [self getHost:NULL port:&port family:NULL fromAddress:address];
+
+ return port;
+}
+
++ (int)familyFromAddress:(NSData *)address
+{
+ int af = AF_UNSPEC;
+ [self getHost:NULL port:NULL family:&af fromAddress:address];
+
+ return af;
+}
+
++ (BOOL)isIPv4Address:(NSData *)address
+{
+ int af = AF_UNSPEC;
+ [self getHost:NULL port:NULL family:&af fromAddress:address];
+
+ return (af == AF_INET);
+}
+
++ (BOOL)isIPv6Address:(NSData *)address
+{
+ int af = AF_UNSPEC;
+ [self getHost:NULL port:NULL family:&af fromAddress:address];
+
+ return (af == AF_INET6);
+}
+
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address
+{
+ return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address];
+}
+
++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address
+{
+ if ([address length] >= sizeof(struct sockaddr))
+ {
+ const struct sockaddr *addrX = (const struct sockaddr *)[address bytes];
+
+ if (addrX->sa_family == AF_INET)
+ {
+ if ([address length] >= sizeof(struct sockaddr_in))
+ {
+ const struct sockaddr_in *addr4 = (const struct sockaddr_in *)addrX;
+
+ if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4];
+ if (portPtr) *portPtr = [self portFromSockaddr4:addr4];
+ if (afPtr) *afPtr = AF_INET;
+
+ return YES;
+ }
+ }
+ else if (addrX->sa_family == AF_INET6)
+ {
+ if ([address length] >= sizeof(struct sockaddr_in6))
+ {
+ const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)addrX;
+
+ if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6];
+ if (portPtr) *portPtr = [self portFromSockaddr6:addr6];
+ if (afPtr) *afPtr = AF_INET6;
+
+ return YES;
+ }
+ }
+ }
+
+ if (hostPtr) *hostPtr = nil;
+ if (portPtr) *portPtr = 0;
+ if (afPtr) *afPtr = AF_UNSPEC;
+
+ return NO;
+}
+
+@end
--- /dev/null
+//
+// NSString+UTF8Data.h
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-8-12.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSString (UTF8Data)
+
++ (NSString *)stringWithUTF8Data:(NSData *)data;
+
+- (NSString *)initWithUTF8Data:(NSData *)data;
+
+- (NSData *)UTF8Data;
+
+@end
--- /dev/null
+//
+// NSString+UTF8Data.m
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-8-12.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import "NSString+UTF8Data.h"
+
+@implementation NSString (UTF8Data)
+
++ (NSString *)stringWithUTF8Data:(NSData *)data
+{
+ return [[NSString alloc] initWithUTF8Data:data];
+}
+
+- (NSString *)initWithUTF8Data:(NSData *)data
+{
+ return [self initWithData:data encoding:NSUTF8StringEncoding];
+}
+
+- (NSData *)UTF8Data
+{
+ return [self dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+@end
--- /dev/null
+//
+// NSURL+relativePath.h
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-8-10.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSURL (relativePath)
+
+- (NSString *)relativePath:(NSURL *)baseURL;
+
+@end
--- /dev/null
+//
+// NSURL+relativePath.m
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-8-10.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import "NSURL+relativePath.h"
+
+@implementation NSURL (relativePath)
+
+- (NSString *)relativePath:(NSURL *)baseURL
+{
+ NSURL *full = [[self standardizedURL] absoluteURL];
+ NSURL *base = [[baseURL standardizedURL] absoluteURL];
+ NSArray *fullComponents = [full pathComponents];
+ NSArray *baseComponents = [base pathComponents];
+ NSUInteger fullCount = [fullComponents count];
+ NSUInteger baseCount = [baseComponents count];
+
+ NSMutableArray *result = [NSMutableArray array];
+ int i = 0;
+ for (; i < baseCount; i++) {
+ if (![fullComponents[i] isEqualTo:baseComponents[i]])
+ break;
+ }
+ for (; i < baseCount; i++)
+ [result addObject:@".."];
+ for (; i < fullCount; i++)
+ [result addObject:fullComponents[i]];
+ return [[NSURL fileURLWithPathComponents:result] relativePath];
+}
+
+@end
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>org.upsuper.playlistsyncer.Server</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>${MACOSX_DEPLOYMENT_TARGET}</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2013年 Xidorn Quan. All rights reserved.</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
--- /dev/null
+//
+// Prefix header for all source files of the 'SyncerServer' target in the 'SyncerServer' project
+//
+
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+#endif
--- /dev/null
+{\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\paperw9840\paperh8400
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
+
+\f0\b\fs24 \cf0 Engineering:
+\b0 \
+ Some people\
+\
+
+\b Human Interface Design:
+\b0 \
+ Some other people\
+\
+
+\b Testing:
+\b0 \
+ Hopefully not nobody\
+\
+
+\b Documentation:
+\b0 \
+ Whoever\
+\
+
+\b With special thanks to:
+\b0 \
+ Mom\
+}
--- /dev/null
+/* Localized versions of Info.plist keys */
+
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
+ <data>
+ <int key="IBDocument.SystemTarget">1080</int>
+ <string key="IBDocument.SystemVersion">12E55</string>
+ <string key="IBDocument.InterfaceBuilderVersion">3084</string>
+ <string key="IBDocument.AppKitVersion">1187.39</string>
+ <string key="IBDocument.HIToolboxVersion">626.00</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginVersions">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="NS.object.0">3084</string>
+ </object>
+ <array key="IBDocument.IntegratedClassDependencies">
+ <string>IBNSLayoutConstraint</string>
+ <string>NSButton</string>
+ <string>NSButtonCell</string>
+ <string>NSCustomObject</string>
+ <string>NSImageCell</string>
+ <string>NSMenu</string>
+ <string>NSMenuItem</string>
+ <string>NSProgressIndicator</string>
+ <string>NSScrollView</string>
+ <string>NSScroller</string>
+ <string>NSTableColumn</string>
+ <string>NSTableHeaderView</string>
+ <string>NSTableView</string>
+ <string>NSTextField</string>
+ <string>NSTextFieldCell</string>
+ <string>NSView</string>
+ <string>NSWindowTemplate</string>
+ </array>
+ <array key="IBDocument.PluginDependencies">
+ <string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+ </array>
+ <object class="NSMutableDictionary" key="IBDocument.Metadata">
+ <string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+ <integer value="1" key="NS.object.0"/>
+ </object>
+ <array class="NSMutableArray" key="IBDocument.RootObjects" id="1048">
+ <object class="NSCustomObject" id="1021">
+ <string key="NSClassName">NSApplication</string>
+ </object>
+ <object class="NSCustomObject" id="1014">
+ <string key="NSClassName">FirstResponder</string>
+ </object>
+ <object class="NSCustomObject" id="1050">
+ <string key="NSClassName">NSApplication</string>
+ </object>
+ <object class="NSMenu" id="649796088">
+ <string key="NSTitle">AMainMenu</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="694149608">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">SyncerServer</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <object class="NSCustomResource" key="NSOnImage" id="35465992">
+ <string key="NSClassName">NSImage</string>
+ <string key="NSResourceName">NSMenuCheckmark</string>
+ </object>
+ <object class="NSCustomResource" key="NSMixedImage" id="502551668">
+ <string key="NSClassName">NSImage</string>
+ <string key="NSResourceName">NSMenuMixedState</string>
+ </object>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="110575045">
+ <string key="NSTitle">SyncerServer</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="238522557">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">About SyncerServer</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="304266470">
+ <reference key="NSMenu" ref="110575045"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="609285721">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Preferences…</string>
+ <string key="NSKeyEquiv">,</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="481834944">
+ <reference key="NSMenu" ref="110575045"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1046388886">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Services</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="752062318">
+ <string key="NSTitle">Services</string>
+ <array class="NSMutableArray" key="NSMenuItems"/>
+ <string key="NSName">_NSServicesMenu</string>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="646227648">
+ <reference key="NSMenu" ref="110575045"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="755159360">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Hide SyncerServer</string>
+ <string key="NSKeyEquiv">h</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="342932134">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Hide Others</string>
+ <string key="NSKeyEquiv">h</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="908899353">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Show All</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1056857174">
+ <reference key="NSMenu" ref="110575045"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="632727374">
+ <reference key="NSMenu" ref="110575045"/>
+ <string key="NSTitle">Quit SyncerServer</string>
+ <string key="NSKeyEquiv">q</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ <string key="NSName">_NSAppleMenu</string>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="379814623">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">File</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="720053764">
+ <string key="NSTitle">File</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="705341025">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">New</string>
+ <string key="NSKeyEquiv">n</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="722745758">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Open…</string>
+ <string key="NSKeyEquiv">o</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1025936716">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Open Recent</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="1065607017">
+ <string key="NSTitle">Open Recent</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="759406840">
+ <reference key="NSMenu" ref="1065607017"/>
+ <string key="NSTitle">Clear Menu</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ <string key="NSName">_NSRecentDocumentsMenu</string>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="425164168">
+ <reference key="NSMenu" ref="720053764"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="776162233">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Close</string>
+ <string key="NSKeyEquiv">w</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1023925487">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Save…</string>
+ <string key="NSKeyEquiv">s</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="579971712">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Revert to Saved</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1010469920">
+ <reference key="NSMenu" ref="720053764"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="294629803">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Page Setup...</string>
+ <string key="NSKeyEquiv">P</string>
+ <int key="NSKeyEquivModMask">1179648</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSToolTip"/>
+ </object>
+ <object class="NSMenuItem" id="49223823">
+ <reference key="NSMenu" ref="720053764"/>
+ <string key="NSTitle">Print…</string>
+ <string key="NSKeyEquiv">p</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="952259628">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">Edit</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="789758025">
+ <string key="NSTitle">Edit</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="1058277027">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Undo</string>
+ <string key="NSKeyEquiv">z</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="790794224">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Redo</string>
+ <string key="NSKeyEquiv">Z</string>
+ <int key="NSKeyEquivModMask">1179648</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1040322652">
+ <reference key="NSMenu" ref="789758025"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="296257095">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Cut</string>
+ <string key="NSKeyEquiv">x</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="860595796">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Copy</string>
+ <string key="NSKeyEquiv">c</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="29853731">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Paste</string>
+ <string key="NSKeyEquiv">v</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="82994268">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Paste and Match Style</string>
+ <string key="NSKeyEquiv">V</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="437104165">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Delete</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="583158037">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Select All</string>
+ <string key="NSKeyEquiv">a</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="212016141">
+ <reference key="NSMenu" ref="789758025"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="892235320">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Find</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="963351320">
+ <string key="NSTitle">Find</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="447796847">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Find…</string>
+ <string key="NSKeyEquiv">f</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">1</int>
+ </object>
+ <object class="NSMenuItem" id="738670835">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Find and Replace…</string>
+ <string key="NSKeyEquiv">f</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">12</int>
+ </object>
+ <object class="NSMenuItem" id="326711663">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Find Next</string>
+ <string key="NSKeyEquiv">g</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">2</int>
+ </object>
+ <object class="NSMenuItem" id="270902937">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Find Previous</string>
+ <string key="NSKeyEquiv">G</string>
+ <int key="NSKeyEquivModMask">1179648</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">3</int>
+ </object>
+ <object class="NSMenuItem" id="159080638">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Use Selection for Find</string>
+ <string key="NSKeyEquiv">e</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">7</int>
+ </object>
+ <object class="NSMenuItem" id="88285865">
+ <reference key="NSMenu" ref="963351320"/>
+ <string key="NSTitle">Jump to Selection</string>
+ <string key="NSKeyEquiv">j</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="972420730">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Spelling and Grammar</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="769623530">
+ <string key="NSTitle">Spelling and Grammar</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="679648819">
+ <reference key="NSMenu" ref="769623530"/>
+ <string key="NSTitle">Show Spelling and Grammar</string>
+ <string key="NSKeyEquiv">:</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="96193923">
+ <reference key="NSMenu" ref="769623530"/>
+ <string key="NSTitle">Check Document Now</string>
+ <string key="NSKeyEquiv">;</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="859480356">
+ <reference key="NSMenu" ref="769623530"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="948374510">
+ <reference key="NSMenu" ref="769623530"/>
+ <string key="NSTitle">Check Spelling While Typing</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="967646866">
+ <reference key="NSMenu" ref="769623530"/>
+ <string key="NSTitle">Check Grammar With Spelling</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="795346622">
+ <reference key="NSMenu" ref="769623530"/>
+ <string key="NSTitle">Correct Spelling Automatically</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="507821607">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Substitutions</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="698887838">
+ <string key="NSTitle">Substitutions</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="65139061">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Show Substitutions</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="19036812">
+ <reference key="NSMenu" ref="698887838"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="605118523">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Smart Copy/Paste</string>
+ <string key="NSKeyEquiv">f</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">1</int>
+ </object>
+ <object class="NSMenuItem" id="197661976">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Smart Quotes</string>
+ <string key="NSKeyEquiv">g</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">2</int>
+ </object>
+ <object class="NSMenuItem" id="672708820">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Smart Dashes</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="708854459">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Smart Links</string>
+ <string key="NSKeyEquiv">G</string>
+ <int key="NSKeyEquivModMask">1179648</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">3</int>
+ </object>
+ <object class="NSMenuItem" id="537092702">
+ <reference key="NSMenu" ref="698887838"/>
+ <string key="NSTitle">Text Replacement</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="288088188">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Transformations</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="579392910">
+ <string key="NSTitle">Transformations</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="1060694897">
+ <reference key="NSMenu" ref="579392910"/>
+ <string key="NSTitle">Make Upper Case</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="879586729">
+ <reference key="NSMenu" ref="579392910"/>
+ <string key="NSTitle">Make Lower Case</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="56570060">
+ <reference key="NSMenu" ref="579392910"/>
+ <string key="NSTitle">Capitalize</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="676164635">
+ <reference key="NSMenu" ref="789758025"/>
+ <string key="NSTitle">Speech</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="785027613">
+ <string key="NSTitle">Speech</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="731782645">
+ <reference key="NSMenu" ref="785027613"/>
+ <string key="NSTitle">Start Speaking</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="680220178">
+ <reference key="NSMenu" ref="785027613"/>
+ <string key="NSTitle">Stop Speaking</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="302598603">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">Format</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="941447902">
+ <string key="NSTitle">Format</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="792887677">
+ <reference key="NSMenu" ref="941447902"/>
+ <string key="NSTitle">Font</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="786677654">
+ <string key="NSTitle">Font</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="159677712">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Show Fonts</string>
+ <string key="NSKeyEquiv">t</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="305399458">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Bold</string>
+ <string key="NSKeyEquiv">b</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">2</int>
+ </object>
+ <object class="NSMenuItem" id="814362025">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Italic</string>
+ <string key="NSKeyEquiv">i</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">1</int>
+ </object>
+ <object class="NSMenuItem" id="330926929">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Underline</string>
+ <string key="NSKeyEquiv">u</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="533507878">
+ <reference key="NSMenu" ref="786677654"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="158063935">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Bigger</string>
+ <string key="NSKeyEquiv">+</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">3</int>
+ </object>
+ <object class="NSMenuItem" id="885547335">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Smaller</string>
+ <string key="NSKeyEquiv">-</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <int key="NSTag">4</int>
+ </object>
+ <object class="NSMenuItem" id="901062459">
+ <reference key="NSMenu" ref="786677654"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="767671776">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Kern</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="175441468">
+ <string key="NSTitle">Kern</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="252969304">
+ <reference key="NSMenu" ref="175441468"/>
+ <string key="NSTitle">Use Default</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="766922938">
+ <reference key="NSMenu" ref="175441468"/>
+ <string key="NSTitle">Use None</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="677519740">
+ <reference key="NSMenu" ref="175441468"/>
+ <string key="NSTitle">Tighten</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="238351151">
+ <reference key="NSMenu" ref="175441468"/>
+ <string key="NSTitle">Loosen</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="691570813">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Ligatures</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="1058217995">
+ <string key="NSTitle">Ligatures</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="706297211">
+ <reference key="NSMenu" ref="1058217995"/>
+ <string key="NSTitle">Use Default</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="568384683">
+ <reference key="NSMenu" ref="1058217995"/>
+ <string key="NSTitle">Use None</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="663508465">
+ <reference key="NSMenu" ref="1058217995"/>
+ <string key="NSTitle">Use All</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="769124883">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Baseline</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="18263474">
+ <string key="NSTitle">Baseline</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="257962622">
+ <reference key="NSMenu" ref="18263474"/>
+ <string key="NSTitle">Use Default</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="644725453">
+ <reference key="NSMenu" ref="18263474"/>
+ <string key="NSTitle">Superscript</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1037576581">
+ <reference key="NSMenu" ref="18263474"/>
+ <string key="NSTitle">Subscript</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="941806246">
+ <reference key="NSMenu" ref="18263474"/>
+ <string key="NSTitle">Raise</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1045724900">
+ <reference key="NSMenu" ref="18263474"/>
+ <string key="NSTitle">Lower</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="739652853">
+ <reference key="NSMenu" ref="786677654"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="1012600125">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Show Colors</string>
+ <string key="NSKeyEquiv">C</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="214559597">
+ <reference key="NSMenu" ref="786677654"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="596732606">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Copy Style</string>
+ <string key="NSKeyEquiv">c</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="393423671">
+ <reference key="NSMenu" ref="786677654"/>
+ <string key="NSTitle">Paste Style</string>
+ <string key="NSKeyEquiv">v</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ <string key="NSName">_NSFontMenu</string>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="215659978">
+ <reference key="NSMenu" ref="941447902"/>
+ <string key="NSTitle">Text</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="446991534">
+ <string key="NSTitle">Text</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="875092757">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Align Left</string>
+ <string key="NSKeyEquiv">{</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="630155264">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Center</string>
+ <string key="NSKeyEquiv">|</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="945678886">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Justify</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="512868991">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Align Right</string>
+ <string key="NSKeyEquiv">}</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="163117631">
+ <reference key="NSMenu" ref="446991534"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="31516759">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Writing Direction</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="956096989">
+ <string key="NSTitle">Writing Direction</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="257099033">
+ <reference key="NSMenu" ref="956096989"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <string key="NSTitle">Paragraph</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="551969625">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CURlZmF1bHQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="249532473">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CUxlZnQgdG8gUmlnaHQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="607364498">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CVJpZ2h0IHRvIExlZnQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="508151438">
+ <reference key="NSMenu" ref="956096989"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="981751889">
+ <reference key="NSMenu" ref="956096989"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <string key="NSTitle">Selection</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="380031999">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CURlZmF1bHQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="825984362">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CUxlZnQgdG8gUmlnaHQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="560145579">
+ <reference key="NSMenu" ref="956096989"/>
+ <string type="base64-UTF8" key="NSTitle">CVJpZ2h0IHRvIExlZnQ</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="908105787">
+ <reference key="NSMenu" ref="446991534"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="644046920">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Show Ruler</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="231811626">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Copy Ruler</string>
+ <string key="NSKeyEquiv">c</string>
+ <int key="NSKeyEquivModMask">1310720</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="883618387">
+ <reference key="NSMenu" ref="446991534"/>
+ <string key="NSTitle">Paste Ruler</string>
+ <string key="NSKeyEquiv">v</string>
+ <int key="NSKeyEquivModMask">1310720</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="586577488">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">View</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="466310130">
+ <string key="NSTitle">View</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="102151532">
+ <reference key="NSMenu" ref="466310130"/>
+ <string key="NSTitle">Show Toolbar</string>
+ <string key="NSKeyEquiv">t</string>
+ <int key="NSKeyEquivModMask">1572864</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="237841660">
+ <reference key="NSMenu" ref="466310130"/>
+ <string key="NSTitle">Customize Toolbar…</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="713487014">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">Window</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="835318025">
+ <string key="NSTitle">Window</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="1011231497">
+ <reference key="NSMenu" ref="835318025"/>
+ <string key="NSTitle">Minimize</string>
+ <string key="NSKeyEquiv">m</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="575023229">
+ <reference key="NSMenu" ref="835318025"/>
+ <string key="NSTitle">Zoom</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="299356726">
+ <reference key="NSMenu" ref="835318025"/>
+ <bool key="NSIsDisabled">YES</bool>
+ <bool key="NSIsSeparator">YES</bool>
+ <string key="NSTitle"/>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ <object class="NSMenuItem" id="625202149">
+ <reference key="NSMenu" ref="835318025"/>
+ <string key="NSTitle">Bring All to Front</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ <string key="NSName">_NSWindowsMenu</string>
+ </object>
+ </object>
+ <object class="NSMenuItem" id="448692316">
+ <reference key="NSMenu" ref="649796088"/>
+ <string key="NSTitle">Help</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="992780483">
+ <string key="NSTitle">Help</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="105068016">
+ <reference key="NSMenu" ref="992780483"/>
+ <string key="NSTitle">SyncerServer Help</string>
+ <string key="NSKeyEquiv">?</string>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <reference key="NSOnImage" ref="35465992"/>
+ <reference key="NSMixedImage" ref="502551668"/>
+ </object>
+ </array>
+ <string key="NSName">_NSHelpMenu</string>
+ </object>
+ </object>
+ </array>
+ <string key="NSName">_NSMainMenu</string>
+ </object>
+ <object class="NSWindowTemplate" id="972006081">
+ <int key="NSWindowStyleMask">15</int>
+ <int key="NSWindowBacking">2</int>
+ <string key="NSWindowRect">{{335, 390}, {507, 366}}</string>
+ <int key="NSWTFlags">1954021376</int>
+ <string key="NSWindowTitle">Playlist Syncer Server</string>
+ <string key="NSWindowClass">NSWindow</string>
+ <nil key="NSViewClass"/>
+ <nil key="NSUserInterfaceItemIdentifier"/>
+ <object class="NSView" key="NSWindowView" id="439893737">
+ <reference key="NSNextResponder"/>
+ <int key="NSvFlags">256</int>
+ <array class="NSMutableArray" key="NSSubviews">
+ <object class="NSScrollView" id="1069725103">
+ <reference key="NSNextResponder" ref="439893737"/>
+ <int key="NSvFlags">268</int>
+ <array class="NSMutableArray" key="NSSubviews">
+ <object class="NSClipView" id="1069908579">
+ <reference key="NSNextResponder" ref="1069725103"/>
+ <int key="NSvFlags">2304</int>
+ <array class="NSMutableArray" key="NSSubviews">
+ <object class="NSTableView" id="433902417">
+ <reference key="NSNextResponder" ref="1069908579"/>
+ <int key="NSvFlags">256</int>
+ <array class="NSMutableArray" key="NSSubviews"/>
+ <string key="NSFrameSize">{465, 280}</string>
+ <reference key="NSSuperview" ref="1069908579"/>
+ <reference key="NSNextKeyView" ref="458425365"/>
+ <string key="NSReuseIdentifierKey">_NS:13</string>
+ <bool key="NSEnabled">YES</bool>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ <bool key="NSControlAllowsExpansionToolTips">YES</bool>
+ <object class="NSTableHeaderView" key="NSHeaderView" id="270582777">
+ <reference key="NSNextResponder" ref="458425365"/>
+ <int key="NSvFlags">256</int>
+ <string key="NSFrameSize">{465, 17}</string>
+ <reference key="NSSuperview" ref="458425365"/>
+ <reference key="NSNextKeyView" ref="1069908579"/>
+ <reference key="NSTableView" ref="433902417"/>
+ </object>
+ <object class="_NSCornerView" key="NSCornerView">
+ <nil key="NSNextResponder"/>
+ <int key="NSvFlags">-2147483392</int>
+ <string key="NSFrame">{{224, 0}, {16, 17}}</string>
+ <reference key="NSNextKeyView" ref="1069908579"/>
+ <string key="NSReuseIdentifierKey">_NS:19</string>
+ </object>
+ <array class="NSMutableArray" key="NSTableColumns">
+ <object class="NSTableColumn" id="343102251">
+ <string key="NSIdentifier">status</string>
+ <double key="NSWidth">40</double>
+ <double key="NSMinWidth">40</double>
+ <double key="NSMaxWidth">1000</double>
+ <object class="NSTableHeaderCell" key="NSHeaderCell">
+ <int key="NSCellFlags">75497536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents"/>
+ <object class="NSFont" key="NSSupport" id="26">
+ <string key="NSName">LucidaGrande</string>
+ <double key="NSSize">11</double>
+ <int key="NSfFlags">3100</int>
+ </object>
+ <object class="NSColor" key="NSBackgroundColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MC4zMzMzMzI5ODU2AA</bytes>
+ </object>
+ <object class="NSColor" key="NSTextColor" id="605161867">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">headerTextColor</string>
+ <object class="NSColor" key="NSColor" id="747335566">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MAA</bytes>
+ </object>
+ </object>
+ </object>
+ <object class="NSImageCell" key="NSDataCell" id="985635909">
+ <int key="NSCellFlags">134217728</int>
+ <int key="NSCellFlags2">33554432</int>
+ <string key="NSCellIdentifier">_NS:9</string>
+ <int key="NSAlign">0</int>
+ <int key="NSScale">0</int>
+ <int key="NSStyle">0</int>
+ <bool key="NSAnimates">NO</bool>
+ </object>
+ <int key="NSResizingMask">3</int>
+ <bool key="NSIsResizeable">YES</bool>
+ <reference key="NSTableView" ref="433902417"/>
+ </object>
+ <object class="NSTableColumn" id="564507632">
+ <string key="NSIdentifier">name</string>
+ <double key="NSWidth">93.8671875</double>
+ <double key="NSMinWidth">10</double>
+ <double key="NSMaxWidth">3.4028234663852886e+38</double>
+ <object class="NSTableHeaderCell" key="NSHeaderCell">
+ <int key="NSCellFlags">75497536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">Name</string>
+ <reference key="NSSupport" ref="26"/>
+ <object class="NSColor" key="NSBackgroundColor" id="224028963">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">headerColor</string>
+ <object class="NSColor" key="NSColor" id="949639230">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MQA</bytes>
+ </object>
+ </object>
+ <reference key="NSTextColor" ref="605161867"/>
+ </object>
+ <object class="NSTextFieldCell" key="NSDataCell" id="895437156">
+ <int key="NSCellFlags">337641536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">Text Cell</string>
+ <object class="NSFont" key="NSSupport" id="735467737">
+ <string key="NSName">LucidaGrande</string>
+ <double key="NSSize">13</double>
+ <int key="NSfFlags">1044</int>
+ </object>
+ <reference key="NSControlView" ref="433902417"/>
+ <object class="NSColor" key="NSBackgroundColor" id="917891807">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">controlBackgroundColor</string>
+ <object class="NSColor" key="NSColor" id="972402753">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MC42NjY2NjY2NjY3AA</bytes>
+ </object>
+ </object>
+ <object class="NSColor" key="NSTextColor" id="28610965">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">controlTextColor</string>
+ <reference key="NSColor" ref="747335566"/>
+ </object>
+ </object>
+ <int key="NSResizingMask">3</int>
+ <bool key="NSIsResizeable">YES</bool>
+ <bool key="NSIsEditable">YES</bool>
+ <reference key="NSTableView" ref="433902417"/>
+ </object>
+ <object class="NSTableColumn" id="349044938">
+ <string key="NSIdentifier">size</string>
+ <double key="NSWidth">65.15234375</double>
+ <double key="NSMinWidth">10</double>
+ <double key="NSMaxWidth">3.4028234663852886e+38</double>
+ <object class="NSTableHeaderCell" key="NSHeaderCell">
+ <int key="NSCellFlags">75497536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">Size</string>
+ <reference key="NSSupport" ref="26"/>
+ <reference key="NSBackgroundColor" ref="224028963"/>
+ <reference key="NSTextColor" ref="605161867"/>
+ </object>
+ <object class="NSTextFieldCell" key="NSDataCell" id="953174591">
+ <int key="NSCellFlags">337641536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">Text Cell</string>
+ <reference key="NSSupport" ref="735467737"/>
+ <reference key="NSControlView" ref="433902417"/>
+ <reference key="NSBackgroundColor" ref="917891807"/>
+ <reference key="NSTextColor" ref="28610965"/>
+ </object>
+ <int key="NSResizingMask">3</int>
+ <bool key="NSIsResizeable">YES</bool>
+ <bool key="NSIsEditable">YES</bool>
+ <reference key="NSTableView" ref="433902417"/>
+ </object>
+ <object class="NSTableColumn" id="622040492">
+ <string key="NSIdentifier">file</string>
+ <double key="NSWidth">240.28515625</double>
+ <double key="NSMinWidth">10</double>
+ <double key="NSMaxWidth">3.4028234663852886e+38</double>
+ <object class="NSTableHeaderCell" key="NSHeaderCell">
+ <int key="NSCellFlags">75497536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">File</string>
+ <reference key="NSSupport" ref="26"/>
+ <reference key="NSBackgroundColor" ref="224028963"/>
+ <reference key="NSTextColor" ref="605161867"/>
+ </object>
+ <object class="NSTextFieldCell" key="NSDataCell" id="1053131822">
+ <int key="NSCellFlags">337641536</int>
+ <int key="NSCellFlags2">2048</int>
+ <string key="NSContents">Text Cell</string>
+ <reference key="NSSupport" ref="735467737"/>
+ <reference key="NSControlView" ref="433902417"/>
+ <reference key="NSBackgroundColor" ref="917891807"/>
+ <reference key="NSTextColor" ref="28610965"/>
+ </object>
+ <int key="NSResizingMask">3</int>
+ <bool key="NSIsResizeable">YES</bool>
+ <bool key="NSIsEditable">YES</bool>
+ <reference key="NSTableView" ref="433902417"/>
+ </object>
+ </array>
+ <double key="NSIntercellSpacingWidth">3</double>
+ <double key="NSIntercellSpacingHeight">2</double>
+ <reference key="NSBackgroundColor" ref="949639230"/>
+ <object class="NSColor" key="NSGridColor">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">gridColor</string>
+ <object class="NSColor" key="NSColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MC41AA</bytes>
+ </object>
+ </object>
+ <double key="NSRowHeight">17</double>
+ <int key="NSTvFlags">1444937728</int>
+ <reference key="NSDelegate"/>
+ <reference key="NSDataSource"/>
+ <int key="NSColumnAutoresizingStyle">4</int>
+ <int key="NSDraggingSourceMaskForLocal">15</int>
+ <int key="NSDraggingSourceMaskForNonLocal">0</int>
+ <bool key="NSAllowsTypeSelect">YES</bool>
+ <int key="NSTableViewDraggingDestinationStyle">0</int>
+ <int key="NSTableViewGroupRowStyle">1</int>
+ </object>
+ </array>
+ <string key="NSFrame">{{1, 17}, {465, 280}}</string>
+ <reference key="NSSuperview" ref="1069725103"/>
+ <reference key="NSNextKeyView" ref="433902417"/>
+ <string key="NSReuseIdentifierKey">_NS:11</string>
+ <reference key="NSDocView" ref="433902417"/>
+ <reference key="NSBGColor" ref="917891807"/>
+ <int key="NScvFlags">4</int>
+ </object>
+ <object class="NSScroller" id="37275419">
+ <reference key="NSNextResponder" ref="1069725103"/>
+ <int key="NSvFlags">-2147483392</int>
+ <string key="NSFrame">{{224, 17}, {15, 102}}</string>
+ <reference key="NSSuperview" ref="1069725103"/>
+ <reference key="NSNextKeyView" ref="928895666"/>
+ <string key="NSReuseIdentifierKey">_NS:58</string>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ <reference key="NSTarget" ref="1069725103"/>
+ <string key="NSAction">_doScroller:</string>
+ <double key="NSPercent">0.99644128113879005</double>
+ </object>
+ <object class="NSScroller" id="928895666">
+ <reference key="NSNextResponder" ref="1069725103"/>
+ <int key="NSvFlags">-2147483392</int>
+ <string key="NSFrame">{{1, 281}, {465, 16}}</string>
+ <reference key="NSSuperview" ref="1069725103"/>
+ <reference key="NSNextKeyView" ref="984844806"/>
+ <string key="NSReuseIdentifierKey">_NS:60</string>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ <int key="NSsFlags">1</int>
+ <reference key="NSTarget" ref="1069725103"/>
+ <string key="NSAction">_doScroller:</string>
+ <double key="NSPercent">0.99785407725321884</double>
+ </object>
+ <object class="NSClipView" id="458425365">
+ <reference key="NSNextResponder" ref="1069725103"/>
+ <int key="NSvFlags">2304</int>
+ <array class="NSMutableArray" key="NSSubviews">
+ <reference ref="270582777"/>
+ </array>
+ <string key="NSFrame">{{1, 0}, {465, 17}}</string>
+ <reference key="NSSuperview" ref="1069725103"/>
+ <reference key="NSNextKeyView" ref="270582777"/>
+ <reference key="NSDocView" ref="270582777"/>
+ <reference key="NSBGColor" ref="917891807"/>
+ <int key="NScvFlags">4</int>
+ </object>
+ </array>
+ <string key="NSFrame">{{20, 48}, {467, 298}}</string>
+ <reference key="NSSuperview" ref="439893737"/>
+ <reference key="NSNextKeyView" ref="1069908579"/>
+ <string key="NSReuseIdentifierKey">_NS:9</string>
+ <int key="NSsFlags">133682</int>
+ <reference key="NSVScroller" ref="37275419"/>
+ <reference key="NSHScroller" ref="928895666"/>
+ <reference key="NSContentView" ref="1069908579"/>
+ <reference key="NSHeaderClipView" ref="458425365"/>
+ <bytes key="NSScrollAmts">QSAAAEEgAABBmAAAQZgAAA</bytes>
+ <double key="NSMinMagnification">0.25</double>
+ <double key="NSMaxMagnification">4</double>
+ <double key="NSMagnification">1</double>
+ </object>
+ <object class="NSButton" id="645018061">
+ <reference key="NSNextResponder" ref="439893737"/>
+ <int key="NSvFlags">268</int>
+ <string key="NSFrame">{{325, 13}, {86, 32}}</string>
+ <reference key="NSSuperview" ref="439893737"/>
+ <reference key="NSNextKeyView" ref="213647002"/>
+ <string key="NSReuseIdentifierKey">_NS:9</string>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSButtonCell" key="NSCell" id="1043354070">
+ <int key="NSCellFlags">603979776</int>
+ <int key="NSCellFlags2">134217728</int>
+ <string key="NSContents">Start</string>
+ <reference key="NSSupport" ref="735467737"/>
+ <string key="NSCellIdentifier">_NS:9</string>
+ <reference key="NSControlView" ref="645018061"/>
+ <int key="NSButtonFlags">-2038284288</int>
+ <int key="NSButtonFlags2">129</int>
+ <string key="NSAlternateContents"/>
+ <string type="base64-UTF8" key="NSKeyEquivalent">DQ</string>
+ <int key="NSPeriodicDelay">200</int>
+ <int key="NSPeriodicInterval">25</int>
+ </object>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ </object>
+ <object class="NSProgressIndicator" id="984844806">
+ <reference key="NSNextResponder" ref="439893737"/>
+ <int key="NSvFlags">268</int>
+ <string key="NSFrame">{{20, 20}, {16, 16}}</string>
+ <reference key="NSSuperview" ref="439893737"/>
+ <reference key="NSNextKeyView" ref="346900410"/>
+ <string key="NSReuseIdentifierKey">_NS:945</string>
+ <int key="NSpiFlags">20746</int>
+ <double key="NSMaxValue">100</double>
+ </object>
+ <object class="NSButton" id="213647002">
+ <reference key="NSNextResponder" ref="439893737"/>
+ <int key="NSvFlags">268</int>
+ <string key="NSFrame">{{411, 13}, {82, 32}}</string>
+ <reference key="NSSuperview" ref="439893737"/>
+ <string key="NSReuseIdentifierKey">_NS:9</string>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSButtonCell" key="NSCell" id="116328002">
+ <int key="NSCellFlags">603979776</int>
+ <int key="NSCellFlags2">134217728</int>
+ <string key="NSContents">Retry</string>
+ <reference key="NSSupport" ref="735467737"/>
+ <string key="NSCellIdentifier">_NS:9</string>
+ <reference key="NSControlView" ref="213647002"/>
+ <int key="NSButtonFlags">-2038284288</int>
+ <int key="NSButtonFlags2">129</int>
+ <reference key="NSAlternateImage" ref="735467737"/>
+ <string key="NSAlternateContents"/>
+ <string key="NSKeyEquivalent"/>
+ <int key="NSPeriodicDelay">200</int>
+ <int key="NSPeriodicInterval">25</int>
+ </object>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ </object>
+ <object class="NSTextField" id="346900410">
+ <reference key="NSNextResponder" ref="439893737"/>
+ <int key="NSvFlags">268</int>
+ <string key="NSFrame">{{41, 20}, {282, 17}}</string>
+ <reference key="NSSuperview" ref="439893737"/>
+ <reference key="NSNextKeyView" ref="645018061"/>
+ <string key="NSReuseIdentifierKey">_NS:1535</string>
+ <bool key="NSEnabled">YES</bool>
+ <object class="NSTextFieldCell" key="NSCell" id="465848076">
+ <int key="NSCellFlags">68157504</int>
+ <int key="NSCellFlags2">272630784</int>
+ <string key="NSContents">Waiting...</string>
+ <reference key="NSSupport" ref="735467737"/>
+ <string key="NSCellIdentifier">_NS:1535</string>
+ <reference key="NSControlView" ref="346900410"/>
+ <object class="NSColor" key="NSBackgroundColor">
+ <int key="NSColorSpace">6</int>
+ <string key="NSCatalogName">System</string>
+ <string key="NSColorName">controlColor</string>
+ <reference key="NSColor" ref="972402753"/>
+ </object>
+ <reference key="NSTextColor" ref="28610965"/>
+ </object>
+ <bool key="NSAllowsLogicalLayoutDirection">NO</bool>
+ </object>
+ </array>
+ <string key="NSFrameSize">{507, 366}</string>
+ <reference key="NSSuperview"/>
+ <reference key="NSNextKeyView" ref="1069725103"/>
+ </object>
+ <string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
+ <string key="NSMaxSize">{10000000000000, 10000000000000}</string>
+ <bool key="NSWindowIsRestorable">YES</bool>
+ </object>
+ <object class="NSCustomObject" id="976324537">
+ <string key="NSClassName">AppDelegate</string>
+ </object>
+ <object class="NSCustomObject" id="755631768">
+ <string key="NSClassName">NSFontManager</string>
+ </object>
+ </array>
+ <object class="IBObjectContainer" key="IBDocument.Objects">
+ <array class="NSMutableArray" key="connectionRecords">
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">terminate:</string>
+ <reference key="source" ref="1050"/>
+ <reference key="destination" ref="632727374"/>
+ </object>
+ <int key="connectionID">449</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">orderFrontStandardAboutPanel:</string>
+ <reference key="source" ref="1021"/>
+ <reference key="destination" ref="238522557"/>
+ </object>
+ <int key="connectionID">142</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">delegate</string>
+ <reference key="source" ref="1021"/>
+ <reference key="destination" ref="976324537"/>
+ </object>
+ <int key="connectionID">495</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performMiniaturize:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1011231497"/>
+ </object>
+ <int key="connectionID">37</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">arrangeInFront:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="625202149"/>
+ </object>
+ <int key="connectionID">39</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">print:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="49223823"/>
+ </object>
+ <int key="connectionID">86</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">runPageLayout:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="294629803"/>
+ </object>
+ <int key="connectionID">87</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">clearRecentDocuments:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="759406840"/>
+ </object>
+ <int key="connectionID">127</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performClose:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="776162233"/>
+ </object>
+ <int key="connectionID">193</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleContinuousSpellChecking:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="948374510"/>
+ </object>
+ <int key="connectionID">222</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">undo:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1058277027"/>
+ </object>
+ <int key="connectionID">223</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">copy:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="860595796"/>
+ </object>
+ <int key="connectionID">224</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">checkSpelling:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="96193923"/>
+ </object>
+ <int key="connectionID">225</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">paste:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="29853731"/>
+ </object>
+ <int key="connectionID">226</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">stopSpeaking:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="680220178"/>
+ </object>
+ <int key="connectionID">227</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">cut:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="296257095"/>
+ </object>
+ <int key="connectionID">228</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">showGuessPanel:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="679648819"/>
+ </object>
+ <int key="connectionID">230</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">redo:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="790794224"/>
+ </object>
+ <int key="connectionID">231</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">selectAll:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="583158037"/>
+ </object>
+ <int key="connectionID">232</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">startSpeaking:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="731782645"/>
+ </object>
+ <int key="connectionID">233</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">delete:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="437104165"/>
+ </object>
+ <int key="connectionID">235</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performZoom:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="575023229"/>
+ </object>
+ <int key="connectionID">240</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performFindPanelAction:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="447796847"/>
+ </object>
+ <int key="connectionID">241</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">centerSelectionInVisibleArea:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="88285865"/>
+ </object>
+ <int key="connectionID">245</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleGrammarChecking:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="967646866"/>
+ </object>
+ <int key="connectionID">347</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleSmartInsertDelete:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="605118523"/>
+ </object>
+ <int key="connectionID">355</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleAutomaticQuoteSubstitution:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="197661976"/>
+ </object>
+ <int key="connectionID">356</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleAutomaticLinkDetection:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="708854459"/>
+ </object>
+ <int key="connectionID">357</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">saveDocument:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1023925487"/>
+ </object>
+ <int key="connectionID">362</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">revertDocumentToSaved:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="579971712"/>
+ </object>
+ <int key="connectionID">364</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">runToolbarCustomizationPalette:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="237841660"/>
+ </object>
+ <int key="connectionID">365</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleToolbarShown:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="102151532"/>
+ </object>
+ <int key="connectionID">366</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">hide:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="755159360"/>
+ </object>
+ <int key="connectionID">367</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">hideOtherApplications:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="342932134"/>
+ </object>
+ <int key="connectionID">368</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">unhideAllApplications:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="908899353"/>
+ </object>
+ <int key="connectionID">370</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">newDocument:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="705341025"/>
+ </object>
+ <int key="connectionID">373</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">openDocument:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="722745758"/>
+ </object>
+ <int key="connectionID">374</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">raiseBaseline:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="941806246"/>
+ </object>
+ <int key="connectionID">426</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">lowerBaseline:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1045724900"/>
+ </object>
+ <int key="connectionID">427</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">copyFont:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="596732606"/>
+ </object>
+ <int key="connectionID">428</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">subscript:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1037576581"/>
+ </object>
+ <int key="connectionID">429</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">superscript:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="644725453"/>
+ </object>
+ <int key="connectionID">430</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">tightenKerning:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="677519740"/>
+ </object>
+ <int key="connectionID">431</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">underline:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="330926929"/>
+ </object>
+ <int key="connectionID">432</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">orderFrontColorPanel:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1012600125"/>
+ </object>
+ <int key="connectionID">433</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">useAllLigatures:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="663508465"/>
+ </object>
+ <int key="connectionID">434</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">loosenKerning:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="238351151"/>
+ </object>
+ <int key="connectionID">435</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">pasteFont:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="393423671"/>
+ </object>
+ <int key="connectionID">436</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">unscript:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="257962622"/>
+ </object>
+ <int key="connectionID">437</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">useStandardKerning:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="252969304"/>
+ </object>
+ <int key="connectionID">438</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">useStandardLigatures:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="706297211"/>
+ </object>
+ <int key="connectionID">439</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">turnOffLigatures:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="568384683"/>
+ </object>
+ <int key="connectionID">440</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">turnOffKerning:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="766922938"/>
+ </object>
+ <int key="connectionID">441</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleAutomaticSpellingCorrection:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="795346622"/>
+ </object>
+ <int key="connectionID">456</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">orderFrontSubstitutionsPanel:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="65139061"/>
+ </object>
+ <int key="connectionID">458</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleAutomaticDashSubstitution:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="672708820"/>
+ </object>
+ <int key="connectionID">461</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleAutomaticTextReplacement:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="537092702"/>
+ </object>
+ <int key="connectionID">463</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">uppercaseWord:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="1060694897"/>
+ </object>
+ <int key="connectionID">464</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">capitalizeWord:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="56570060"/>
+ </object>
+ <int key="connectionID">467</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">lowercaseWord:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="879586729"/>
+ </object>
+ <int key="connectionID">468</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">pasteAsPlainText:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="82994268"/>
+ </object>
+ <int key="connectionID">486</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performFindPanelAction:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="326711663"/>
+ </object>
+ <int key="connectionID">487</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performFindPanelAction:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="270902937"/>
+ </object>
+ <int key="connectionID">488</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performFindPanelAction:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="159080638"/>
+ </object>
+ <int key="connectionID">489</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">showHelp:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="105068016"/>
+ </object>
+ <int key="connectionID">493</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">alignCenter:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="630155264"/>
+ </object>
+ <int key="connectionID">518</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">pasteRuler:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="883618387"/>
+ </object>
+ <int key="connectionID">519</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">toggleRuler:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="644046920"/>
+ </object>
+ <int key="connectionID">520</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">alignRight:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="512868991"/>
+ </object>
+ <int key="connectionID">521</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">copyRuler:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="231811626"/>
+ </object>
+ <int key="connectionID">522</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">alignJustified:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="945678886"/>
+ </object>
+ <int key="connectionID">523</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">alignLeft:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="875092757"/>
+ </object>
+ <int key="connectionID">524</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeBaseWritingDirectionNatural:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="551969625"/>
+ </object>
+ <int key="connectionID">525</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeBaseWritingDirectionLeftToRight:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="249532473"/>
+ </object>
+ <int key="connectionID">526</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeBaseWritingDirectionRightToLeft:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="607364498"/>
+ </object>
+ <int key="connectionID">527</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeTextWritingDirectionNatural:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="380031999"/>
+ </object>
+ <int key="connectionID">528</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeTextWritingDirectionLeftToRight:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="825984362"/>
+ </object>
+ <int key="connectionID">529</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">makeTextWritingDirectionRightToLeft:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="560145579"/>
+ </object>
+ <int key="connectionID">530</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">performFindPanelAction:</string>
+ <reference key="source" ref="1014"/>
+ <reference key="destination" ref="738670835"/>
+ </object>
+ <int key="connectionID">535</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">addFontTrait:</string>
+ <reference key="source" ref="755631768"/>
+ <reference key="destination" ref="305399458"/>
+ </object>
+ <int key="connectionID">421</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">addFontTrait:</string>
+ <reference key="source" ref="755631768"/>
+ <reference key="destination" ref="814362025"/>
+ </object>
+ <int key="connectionID">422</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">modifyFont:</string>
+ <reference key="source" ref="755631768"/>
+ <reference key="destination" ref="885547335"/>
+ </object>
+ <int key="connectionID">423</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">orderFrontFontPanel:</string>
+ <reference key="source" ref="755631768"/>
+ <reference key="destination" ref="159677712"/>
+ </object>
+ <int key="connectionID">424</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">modifyFont:</string>
+ <reference key="source" ref="755631768"/>
+ <reference key="destination" ref="158063935"/>
+ </object>
+ <int key="connectionID">425</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">window</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="972006081"/>
+ </object>
+ <int key="connectionID">532</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">progressIndicator</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="984844806"/>
+ </object>
+ <int key="connectionID">588</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">startButton</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="645018061"/>
+ </object>
+ <int key="connectionID">619</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">retryButton</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="213647002"/>
+ </object>
+ <int key="connectionID">620</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">statusLabel</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="346900410"/>
+ </object>
+ <int key="connectionID">621</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">trackTable</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="433902417"/>
+ </object>
+ <int key="connectionID">630</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">doRetry:</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="213647002"/>
+ </object>
+ <int key="connectionID">634</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBActionConnection" key="connection">
+ <string key="label">startSync:</string>
+ <reference key="source" ref="976324537"/>
+ <reference key="destination" ref="645018061"/>
+ </object>
+ <int key="connectionID">635</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBOutletConnection" key="connection">
+ <string key="label">dataSource</string>
+ <reference key="source" ref="433902417"/>
+ <reference key="destination" ref="976324537"/>
+ </object>
+ <int key="connectionID">576</int>
+ </object>
+ </array>
+ <object class="IBMutableOrderedSet" key="objectRecords">
+ <array key="orderedObjects">
+ <object class="IBObjectRecord">
+ <int key="objectID">0</int>
+ <array key="object" id="0"/>
+ <reference key="children" ref="1048"/>
+ <nil key="parent"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-2</int>
+ <reference key="object" ref="1021"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">File's Owner</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-1</int>
+ <reference key="object" ref="1014"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">First Responder</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-3</int>
+ <reference key="object" ref="1050"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">Application</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">29</int>
+ <reference key="object" ref="649796088"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="713487014"/>
+ <reference ref="694149608"/>
+ <reference ref="952259628"/>
+ <reference ref="379814623"/>
+ <reference ref="586577488"/>
+ <reference ref="302598603"/>
+ <reference ref="448692316"/>
+ </array>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">19</int>
+ <reference key="object" ref="713487014"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="835318025"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">56</int>
+ <reference key="object" ref="694149608"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="110575045"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">217</int>
+ <reference key="object" ref="952259628"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="789758025"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">83</int>
+ <reference key="object" ref="379814623"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="720053764"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">81</int>
+ <reference key="object" ref="720053764"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1023925487"/>
+ <reference ref="49223823"/>
+ <reference ref="722745758"/>
+ <reference ref="705341025"/>
+ <reference ref="1025936716"/>
+ <reference ref="294629803"/>
+ <reference ref="776162233"/>
+ <reference ref="425164168"/>
+ <reference ref="579971712"/>
+ <reference ref="1010469920"/>
+ </array>
+ <reference key="parent" ref="379814623"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">75</int>
+ <reference key="object" ref="1023925487"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">78</int>
+ <reference key="object" ref="49223823"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">72</int>
+ <reference key="object" ref="722745758"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">82</int>
+ <reference key="object" ref="705341025"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">124</int>
+ <reference key="object" ref="1025936716"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1065607017"/>
+ </array>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">77</int>
+ <reference key="object" ref="294629803"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">73</int>
+ <reference key="object" ref="776162233"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">79</int>
+ <reference key="object" ref="425164168"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">112</int>
+ <reference key="object" ref="579971712"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">74</int>
+ <reference key="object" ref="1010469920"/>
+ <reference key="parent" ref="720053764"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">125</int>
+ <reference key="object" ref="1065607017"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="759406840"/>
+ </array>
+ <reference key="parent" ref="1025936716"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">126</int>
+ <reference key="object" ref="759406840"/>
+ <reference key="parent" ref="1065607017"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">205</int>
+ <reference key="object" ref="789758025"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="437104165"/>
+ <reference ref="583158037"/>
+ <reference ref="1058277027"/>
+ <reference ref="212016141"/>
+ <reference ref="296257095"/>
+ <reference ref="29853731"/>
+ <reference ref="860595796"/>
+ <reference ref="1040322652"/>
+ <reference ref="790794224"/>
+ <reference ref="892235320"/>
+ <reference ref="972420730"/>
+ <reference ref="676164635"/>
+ <reference ref="507821607"/>
+ <reference ref="288088188"/>
+ <reference ref="82994268"/>
+ </array>
+ <reference key="parent" ref="952259628"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">202</int>
+ <reference key="object" ref="437104165"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">198</int>
+ <reference key="object" ref="583158037"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">207</int>
+ <reference key="object" ref="1058277027"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">214</int>
+ <reference key="object" ref="212016141"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">199</int>
+ <reference key="object" ref="296257095"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">203</int>
+ <reference key="object" ref="29853731"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">197</int>
+ <reference key="object" ref="860595796"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">206</int>
+ <reference key="object" ref="1040322652"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">215</int>
+ <reference key="object" ref="790794224"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">218</int>
+ <reference key="object" ref="892235320"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="963351320"/>
+ </array>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">216</int>
+ <reference key="object" ref="972420730"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="769623530"/>
+ </array>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">200</int>
+ <reference key="object" ref="769623530"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="948374510"/>
+ <reference ref="96193923"/>
+ <reference ref="679648819"/>
+ <reference ref="967646866"/>
+ <reference ref="859480356"/>
+ <reference ref="795346622"/>
+ </array>
+ <reference key="parent" ref="972420730"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">219</int>
+ <reference key="object" ref="948374510"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">201</int>
+ <reference key="object" ref="96193923"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">204</int>
+ <reference key="object" ref="679648819"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">220</int>
+ <reference key="object" ref="963351320"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="270902937"/>
+ <reference ref="88285865"/>
+ <reference ref="159080638"/>
+ <reference ref="326711663"/>
+ <reference ref="447796847"/>
+ <reference ref="738670835"/>
+ </array>
+ <reference key="parent" ref="892235320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">213</int>
+ <reference key="object" ref="270902937"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">210</int>
+ <reference key="object" ref="88285865"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">221</int>
+ <reference key="object" ref="159080638"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">208</int>
+ <reference key="object" ref="326711663"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">209</int>
+ <reference key="object" ref="447796847"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">57</int>
+ <reference key="object" ref="110575045"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="238522557"/>
+ <reference ref="755159360"/>
+ <reference ref="908899353"/>
+ <reference ref="632727374"/>
+ <reference ref="646227648"/>
+ <reference ref="609285721"/>
+ <reference ref="481834944"/>
+ <reference ref="304266470"/>
+ <reference ref="1046388886"/>
+ <reference ref="1056857174"/>
+ <reference ref="342932134"/>
+ </array>
+ <reference key="parent" ref="694149608"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">58</int>
+ <reference key="object" ref="238522557"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">134</int>
+ <reference key="object" ref="755159360"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">150</int>
+ <reference key="object" ref="908899353"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">136</int>
+ <reference key="object" ref="632727374"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">144</int>
+ <reference key="object" ref="646227648"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">129</int>
+ <reference key="object" ref="609285721"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">143</int>
+ <reference key="object" ref="481834944"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">236</int>
+ <reference key="object" ref="304266470"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">131</int>
+ <reference key="object" ref="1046388886"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="752062318"/>
+ </array>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">149</int>
+ <reference key="object" ref="1056857174"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">145</int>
+ <reference key="object" ref="342932134"/>
+ <reference key="parent" ref="110575045"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">130</int>
+ <reference key="object" ref="752062318"/>
+ <reference key="parent" ref="1046388886"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">24</int>
+ <reference key="object" ref="835318025"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="299356726"/>
+ <reference ref="625202149"/>
+ <reference ref="575023229"/>
+ <reference ref="1011231497"/>
+ </array>
+ <reference key="parent" ref="713487014"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">92</int>
+ <reference key="object" ref="299356726"/>
+ <reference key="parent" ref="835318025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">5</int>
+ <reference key="object" ref="625202149"/>
+ <reference key="parent" ref="835318025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">239</int>
+ <reference key="object" ref="575023229"/>
+ <reference key="parent" ref="835318025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">23</int>
+ <reference key="object" ref="1011231497"/>
+ <reference key="parent" ref="835318025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">295</int>
+ <reference key="object" ref="586577488"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="466310130"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">296</int>
+ <reference key="object" ref="466310130"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="102151532"/>
+ <reference ref="237841660"/>
+ </array>
+ <reference key="parent" ref="586577488"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">297</int>
+ <reference key="object" ref="102151532"/>
+ <reference key="parent" ref="466310130"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">298</int>
+ <reference key="object" ref="237841660"/>
+ <reference key="parent" ref="466310130"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">211</int>
+ <reference key="object" ref="676164635"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="785027613"/>
+ </array>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">212</int>
+ <reference key="object" ref="785027613"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="680220178"/>
+ <reference ref="731782645"/>
+ </array>
+ <reference key="parent" ref="676164635"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">195</int>
+ <reference key="object" ref="680220178"/>
+ <reference key="parent" ref="785027613"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">196</int>
+ <reference key="object" ref="731782645"/>
+ <reference key="parent" ref="785027613"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">346</int>
+ <reference key="object" ref="967646866"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">348</int>
+ <reference key="object" ref="507821607"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="698887838"/>
+ </array>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">349</int>
+ <reference key="object" ref="698887838"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="605118523"/>
+ <reference ref="197661976"/>
+ <reference ref="708854459"/>
+ <reference ref="65139061"/>
+ <reference ref="19036812"/>
+ <reference ref="672708820"/>
+ <reference ref="537092702"/>
+ </array>
+ <reference key="parent" ref="507821607"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">350</int>
+ <reference key="object" ref="605118523"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">351</int>
+ <reference key="object" ref="197661976"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">354</int>
+ <reference key="object" ref="708854459"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">371</int>
+ <reference key="object" ref="972006081"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="439893737"/>
+ </array>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">372</int>
+ <reference key="object" ref="439893737"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1069725103"/>
+ <object class="IBNSLayoutConstraint" id="991254765">
+ <reference key="firstItem" ref="213647002"/>
+ <int key="firstAttribute">5</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="645018061"/>
+ <int key="secondAttribute">6</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">12</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">6</int>
+ <float key="scoringTypeFloat">24</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="604734182">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">4</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="213647002"/>
+ <int key="secondAttribute">4</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="136619150">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">6</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="213647002"/>
+ <int key="secondAttribute">6</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="641429233">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">4</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="645018061"/>
+ <int key="secondAttribute">4</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="989908945">
+ <reference key="firstItem" ref="346900410"/>
+ <int key="firstAttribute">5</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="984844806"/>
+ <int key="secondAttribute">6</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">8</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">6</int>
+ <float key="scoringTypeFloat">24</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="526335865">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">4</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="346900410"/>
+ <int key="secondAttribute">4</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="737082770">
+ <reference key="firstItem" ref="984844806"/>
+ <int key="firstAttribute">5</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="439893737"/>
+ <int key="secondAttribute">5</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="617628329">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">4</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="984844806"/>
+ <int key="secondAttribute">4</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="856274488">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">4</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="1069725103"/>
+ <int key="secondAttribute">4</int>
+ <float key="multiplier">1</float>
+ <object class="IBLayoutConstant" key="constant">
+ <double key="value">48</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">3</int>
+ <float key="scoringTypeFloat">9</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="519303071">
+ <reference key="firstItem" ref="439893737"/>
+ <int key="firstAttribute">6</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="1069725103"/>
+ <int key="secondAttribute">6</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="927877776">
+ <reference key="firstItem" ref="1069725103"/>
+ <int key="firstAttribute">5</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="439893737"/>
+ <int key="secondAttribute">5</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <object class="IBNSLayoutConstraint" id="393730369">
+ <reference key="firstItem" ref="1069725103"/>
+ <int key="firstAttribute">3</int>
+ <int key="relation">0</int>
+ <reference key="secondItem" ref="439893737"/>
+ <int key="secondAttribute">3</int>
+ <float key="multiplier">1</float>
+ <object class="IBNSLayoutSymbolicConstant" key="constant">
+ <double key="value">20</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="439893737"/>
+ <int key="scoringType">8</int>
+ <float key="scoringTypeFloat">29</float>
+ <int key="contentType">3</int>
+ </object>
+ <reference ref="213647002"/>
+ <reference ref="645018061"/>
+ <reference ref="984844806"/>
+ <reference ref="346900410"/>
+ </array>
+ <reference key="parent" ref="972006081"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">375</int>
+ <reference key="object" ref="302598603"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="941447902"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">376</int>
+ <reference key="object" ref="941447902"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="792887677"/>
+ <reference ref="215659978"/>
+ </array>
+ <reference key="parent" ref="302598603"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">377</int>
+ <reference key="object" ref="792887677"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="786677654"/>
+ </array>
+ <reference key="parent" ref="941447902"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">388</int>
+ <reference key="object" ref="786677654"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="159677712"/>
+ <reference ref="305399458"/>
+ <reference ref="814362025"/>
+ <reference ref="330926929"/>
+ <reference ref="533507878"/>
+ <reference ref="158063935"/>
+ <reference ref="885547335"/>
+ <reference ref="901062459"/>
+ <reference ref="767671776"/>
+ <reference ref="691570813"/>
+ <reference ref="769124883"/>
+ <reference ref="739652853"/>
+ <reference ref="1012600125"/>
+ <reference ref="214559597"/>
+ <reference ref="596732606"/>
+ <reference ref="393423671"/>
+ </array>
+ <reference key="parent" ref="792887677"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">389</int>
+ <reference key="object" ref="159677712"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">390</int>
+ <reference key="object" ref="305399458"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">391</int>
+ <reference key="object" ref="814362025"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">392</int>
+ <reference key="object" ref="330926929"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">393</int>
+ <reference key="object" ref="533507878"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">394</int>
+ <reference key="object" ref="158063935"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">395</int>
+ <reference key="object" ref="885547335"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">396</int>
+ <reference key="object" ref="901062459"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">397</int>
+ <reference key="object" ref="767671776"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="175441468"/>
+ </array>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">398</int>
+ <reference key="object" ref="691570813"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1058217995"/>
+ </array>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">399</int>
+ <reference key="object" ref="769124883"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="18263474"/>
+ </array>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">400</int>
+ <reference key="object" ref="739652853"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">401</int>
+ <reference key="object" ref="1012600125"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">402</int>
+ <reference key="object" ref="214559597"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">403</int>
+ <reference key="object" ref="596732606"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">404</int>
+ <reference key="object" ref="393423671"/>
+ <reference key="parent" ref="786677654"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">405</int>
+ <reference key="object" ref="18263474"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="257962622"/>
+ <reference ref="644725453"/>
+ <reference ref="1037576581"/>
+ <reference ref="941806246"/>
+ <reference ref="1045724900"/>
+ </array>
+ <reference key="parent" ref="769124883"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">406</int>
+ <reference key="object" ref="257962622"/>
+ <reference key="parent" ref="18263474"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">407</int>
+ <reference key="object" ref="644725453"/>
+ <reference key="parent" ref="18263474"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">408</int>
+ <reference key="object" ref="1037576581"/>
+ <reference key="parent" ref="18263474"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">409</int>
+ <reference key="object" ref="941806246"/>
+ <reference key="parent" ref="18263474"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">410</int>
+ <reference key="object" ref="1045724900"/>
+ <reference key="parent" ref="18263474"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">411</int>
+ <reference key="object" ref="1058217995"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="706297211"/>
+ <reference ref="568384683"/>
+ <reference ref="663508465"/>
+ </array>
+ <reference key="parent" ref="691570813"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">412</int>
+ <reference key="object" ref="706297211"/>
+ <reference key="parent" ref="1058217995"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">413</int>
+ <reference key="object" ref="568384683"/>
+ <reference key="parent" ref="1058217995"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">414</int>
+ <reference key="object" ref="663508465"/>
+ <reference key="parent" ref="1058217995"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">415</int>
+ <reference key="object" ref="175441468"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="252969304"/>
+ <reference ref="766922938"/>
+ <reference ref="677519740"/>
+ <reference ref="238351151"/>
+ </array>
+ <reference key="parent" ref="767671776"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">416</int>
+ <reference key="object" ref="252969304"/>
+ <reference key="parent" ref="175441468"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">417</int>
+ <reference key="object" ref="766922938"/>
+ <reference key="parent" ref="175441468"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">418</int>
+ <reference key="object" ref="677519740"/>
+ <reference key="parent" ref="175441468"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">419</int>
+ <reference key="object" ref="238351151"/>
+ <reference key="parent" ref="175441468"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">420</int>
+ <reference key="object" ref="755631768"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">450</int>
+ <reference key="object" ref="288088188"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="579392910"/>
+ </array>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">451</int>
+ <reference key="object" ref="579392910"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1060694897"/>
+ <reference ref="879586729"/>
+ <reference ref="56570060"/>
+ </array>
+ <reference key="parent" ref="288088188"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">452</int>
+ <reference key="object" ref="1060694897"/>
+ <reference key="parent" ref="579392910"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">453</int>
+ <reference key="object" ref="859480356"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">454</int>
+ <reference key="object" ref="795346622"/>
+ <reference key="parent" ref="769623530"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">457</int>
+ <reference key="object" ref="65139061"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">459</int>
+ <reference key="object" ref="19036812"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">460</int>
+ <reference key="object" ref="672708820"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">462</int>
+ <reference key="object" ref="537092702"/>
+ <reference key="parent" ref="698887838"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">465</int>
+ <reference key="object" ref="879586729"/>
+ <reference key="parent" ref="579392910"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">466</int>
+ <reference key="object" ref="56570060"/>
+ <reference key="parent" ref="579392910"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">485</int>
+ <reference key="object" ref="82994268"/>
+ <reference key="parent" ref="789758025"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">490</int>
+ <reference key="object" ref="448692316"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="992780483"/>
+ </array>
+ <reference key="parent" ref="649796088"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">491</int>
+ <reference key="object" ref="992780483"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="105068016"/>
+ </array>
+ <reference key="parent" ref="448692316"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">492</int>
+ <reference key="object" ref="105068016"/>
+ <reference key="parent" ref="992780483"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">494</int>
+ <reference key="object" ref="976324537"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">496</int>
+ <reference key="object" ref="215659978"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="446991534"/>
+ </array>
+ <reference key="parent" ref="941447902"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">497</int>
+ <reference key="object" ref="446991534"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="875092757"/>
+ <reference ref="630155264"/>
+ <reference ref="945678886"/>
+ <reference ref="512868991"/>
+ <reference ref="163117631"/>
+ <reference ref="31516759"/>
+ <reference ref="908105787"/>
+ <reference ref="644046920"/>
+ <reference ref="231811626"/>
+ <reference ref="883618387"/>
+ </array>
+ <reference key="parent" ref="215659978"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">498</int>
+ <reference key="object" ref="875092757"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">499</int>
+ <reference key="object" ref="630155264"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">500</int>
+ <reference key="object" ref="945678886"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">501</int>
+ <reference key="object" ref="512868991"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">502</int>
+ <reference key="object" ref="163117631"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">503</int>
+ <reference key="object" ref="31516759"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="956096989"/>
+ </array>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">504</int>
+ <reference key="object" ref="908105787"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">505</int>
+ <reference key="object" ref="644046920"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">506</int>
+ <reference key="object" ref="231811626"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">507</int>
+ <reference key="object" ref="883618387"/>
+ <reference key="parent" ref="446991534"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">508</int>
+ <reference key="object" ref="956096989"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="257099033"/>
+ <reference ref="551969625"/>
+ <reference ref="249532473"/>
+ <reference ref="607364498"/>
+ <reference ref="508151438"/>
+ <reference ref="981751889"/>
+ <reference ref="380031999"/>
+ <reference ref="825984362"/>
+ <reference ref="560145579"/>
+ </array>
+ <reference key="parent" ref="31516759"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">509</int>
+ <reference key="object" ref="257099033"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">510</int>
+ <reference key="object" ref="551969625"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">511</int>
+ <reference key="object" ref="249532473"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">512</int>
+ <reference key="object" ref="607364498"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">513</int>
+ <reference key="object" ref="508151438"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">514</int>
+ <reference key="object" ref="981751889"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">515</int>
+ <reference key="object" ref="380031999"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">516</int>
+ <reference key="object" ref="825984362"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">517</int>
+ <reference key="object" ref="560145579"/>
+ <reference key="parent" ref="956096989"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">534</int>
+ <reference key="object" ref="738670835"/>
+ <reference key="parent" ref="963351320"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">536</int>
+ <reference key="object" ref="1069725103"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="433902417"/>
+ <reference ref="928895666"/>
+ <reference ref="37275419"/>
+ <reference ref="270582777"/>
+ </array>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">537</int>
+ <reference key="object" ref="433902417"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="343102251"/>
+ <reference ref="564507632"/>
+ <reference ref="349044938"/>
+ <reference ref="622040492"/>
+ </array>
+ <reference key="parent" ref="1069725103"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">538</int>
+ <reference key="object" ref="928895666"/>
+ <reference key="parent" ref="1069725103"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">540</int>
+ <reference key="object" ref="37275419"/>
+ <reference key="parent" ref="1069725103"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">541</int>
+ <reference key="object" ref="343102251"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="985635909"/>
+ </array>
+ <reference key="parent" ref="433902417"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">545</int>
+ <reference key="object" ref="393730369"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">549</int>
+ <reference key="object" ref="519303071"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">554</int>
+ <reference key="object" ref="645018061"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1043354070"/>
+ <object class="IBNSLayoutConstraint" id="2329200">
+ <reference key="firstItem" ref="645018061"/>
+ <int key="firstAttribute">7</int>
+ <int key="relation">0</int>
+ <nil key="secondItem"/>
+ <int key="secondAttribute">0</int>
+ <float key="multiplier">1</float>
+ <object class="IBLayoutConstant" key="constant">
+ <double key="value">74</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="645018061"/>
+ <int key="scoringType">3</int>
+ <float key="scoringTypeFloat">9</float>
+ <int key="contentType">1</int>
+ </object>
+ </array>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">555</int>
+ <reference key="object" ref="1043354070"/>
+ <reference key="parent" ref="645018061"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">558</int>
+ <reference key="object" ref="2329200"/>
+ <reference key="parent" ref="645018061"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">592</int>
+ <reference key="object" ref="213647002"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="116328002"/>
+ <object class="IBNSLayoutConstraint" id="129574822">
+ <reference key="firstItem" ref="213647002"/>
+ <int key="firstAttribute">7</int>
+ <int key="relation">0</int>
+ <nil key="secondItem"/>
+ <int key="secondAttribute">0</int>
+ <float key="multiplier">1</float>
+ <object class="IBLayoutConstant" key="constant">
+ <double key="value">70</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="213647002"/>
+ <int key="scoringType">3</int>
+ <float key="scoringTypeFloat">9</float>
+ <int key="contentType">1</int>
+ </object>
+ </array>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">593</int>
+ <reference key="object" ref="116328002"/>
+ <reference key="parent" ref="213647002"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">594</int>
+ <reference key="object" ref="136619150"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">585</int>
+ <reference key="object" ref="984844806"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">602</int>
+ <reference key="object" ref="604734182"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">603</int>
+ <reference key="object" ref="856274488"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">604</int>
+ <reference key="object" ref="641429233"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">606</int>
+ <reference key="object" ref="617628329"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">607</int>
+ <reference key="object" ref="737082770"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">605</int>
+ <reference key="object" ref="991254765"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">611</int>
+ <reference key="object" ref="346900410"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="465848076"/>
+ <object class="IBNSLayoutConstraint" id="784985240">
+ <reference key="firstItem" ref="346900410"/>
+ <int key="firstAttribute">7</int>
+ <int key="relation">0</int>
+ <nil key="secondItem"/>
+ <int key="secondAttribute">0</int>
+ <float key="multiplier">1</float>
+ <object class="IBLayoutConstant" key="constant">
+ <double key="value">276</double>
+ </object>
+ <float key="priority">1000</float>
+ <reference key="containingView" ref="346900410"/>
+ <int key="scoringType">3</int>
+ <float key="scoringTypeFloat">9</float>
+ <int key="contentType">1</int>
+ </object>
+ </array>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">612</int>
+ <reference key="object" ref="465848076"/>
+ <reference key="parent" ref="346900410"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">613</int>
+ <reference key="object" ref="526335865"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">615</int>
+ <reference key="object" ref="129574822"/>
+ <reference key="parent" ref="213647002"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">617</int>
+ <reference key="object" ref="784985240"/>
+ <reference key="parent" ref="346900410"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">548</int>
+ <reference key="object" ref="927877776"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">623</int>
+ <reference key="object" ref="270582777"/>
+ <reference key="parent" ref="1069725103"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">624</int>
+ <reference key="object" ref="564507632"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="895437156"/>
+ </array>
+ <reference key="parent" ref="433902417"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">625</int>
+ <reference key="object" ref="895437156"/>
+ <reference key="parent" ref="564507632"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">626</int>
+ <reference key="object" ref="349044938"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="953174591"/>
+ </array>
+ <reference key="parent" ref="433902417"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">627</int>
+ <reference key="object" ref="953174591"/>
+ <reference key="parent" ref="349044938"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">628</int>
+ <reference key="object" ref="985635909"/>
+ <reference key="parent" ref="343102251"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">631</int>
+ <reference key="object" ref="622040492"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="1053131822"/>
+ </array>
+ <reference key="parent" ref="433902417"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">632</int>
+ <reference key="object" ref="1053131822"/>
+ <reference key="parent" ref="622040492"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">633</int>
+ <reference key="object" ref="989908945"/>
+ <reference key="parent" ref="439893737"/>
+ </object>
+ </array>
+ </object>
+ <dictionary class="NSMutableDictionary" key="flattenedProperties">
+ <string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="112.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="124.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="125.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="126.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="129.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="130.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="131.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="134.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="136.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="143.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="144.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="145.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="149.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="150.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="19.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="195.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="196.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="197.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="198.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="199.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="200.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="201.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="202.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="203.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="204.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="205.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="206.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="207.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="208.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="209.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="210.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="211.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="212.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="213.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="214.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="215.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="216.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="217.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="218.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="219.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="220.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="221.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="23.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="236.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="239.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="24.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="295.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="296.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="297.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="298.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="346.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="348.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="349.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="350.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="351.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="354.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="371.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="371.IBWindowTemplateEditedContentRect">{{380, 496}, {480, 360}}</string>
+ <integer value="1" key="371.NSWindowTemplate.visibleAtLaunch"/>
+ <array class="NSMutableArray" key="372.IBNSViewMetadataConstraints">
+ <reference ref="393730369"/>
+ <reference ref="927877776"/>
+ <reference ref="519303071"/>
+ <reference ref="856274488"/>
+ <reference ref="617628329"/>
+ <reference ref="737082770"/>
+ <reference ref="526335865"/>
+ <reference ref="989908945"/>
+ <reference ref="641429233"/>
+ <reference ref="136619150"/>
+ <reference ref="604734182"/>
+ <reference ref="991254765"/>
+ </array>
+ <string key="372.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="375.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="376.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="377.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="388.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="389.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="390.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="391.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="392.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="393.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="394.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="395.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="396.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="397.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="398.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="399.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="400.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="401.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="402.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="403.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="404.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="405.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="406.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="407.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="408.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="409.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="410.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="411.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="412.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="413.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="414.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="415.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="416.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="417.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="418.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="419.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="420.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="450.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="451.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="452.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="453.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="454.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="457.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="459.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="460.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="462.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="465.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="466.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="485.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="490.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="491.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="492.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="494.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="496.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="497.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="498.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="499.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="5.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="500.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="501.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="502.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="503.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="504.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="505.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="506.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="507.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="508.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="509.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="510.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="511.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="512.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="513.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="514.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="515.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="516.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="517.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="534.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <boolean value="NO" key="536.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
+ <string key="536.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="537.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="538.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="540.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="541.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="545.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="548.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="549.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <array key="554.IBNSViewMetadataConstraints">
+ <reference ref="2329200"/>
+ </array>
+ <boolean value="NO" key="554.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
+ <string key="554.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="555.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="558.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="56.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="57.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="58.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <boolean value="NO" key="585.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
+ <string key="585.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <array key="592.IBNSViewMetadataConstraints">
+ <reference ref="129574822"/>
+ </array>
+ <boolean value="NO" key="592.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
+ <string key="592.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="593.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="594.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="602.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="603.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="604.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="605.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="606.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="607.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <array key="611.IBNSViewMetadataConstraints">
+ <reference ref="784985240"/>
+ </array>
+ <boolean value="NO" key="611.IBNSViewMetadataTranslatesAutoresizingMaskIntoConstraints"/>
+ <string key="611.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="612.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="613.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="615.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="617.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="623.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="624.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="625.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="626.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="627.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="628.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="631.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="632.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="633.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="72.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="73.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="74.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="75.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="77.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="78.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="79.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="81.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="82.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="83.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="92.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ </dictionary>
+ <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
+ <nil key="activeLocalization"/>
+ <dictionary class="NSMutableDictionary" key="localizations"/>
+ <nil key="sourceID"/>
+ <int key="maxID">635</int>
+ </object>
+ <object class="IBClassDescriber" key="IBDocument.Classes"/>
+ <int key="IBDocument.localizationMode">0</int>
+ <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
+ <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
+ <int key="IBDocument.defaultPropertyAccessControl">3</int>
+ <dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
+ <string key="NSMenuCheckmark">{11, 11}</string>
+ <string key="NSMenuMixedState">{10, 3}</string>
+ </dictionary>
+ <bool key="IBDocument.UseAutolayout">YES</bool>
+ </data>
+</archive>
--- /dev/null
+//
+// main.m
+// SyncerServer
+//
+// Created by Xidorn Quan on 13-7-9.
+// Copyright (c) 2013年 Xidorn Quan. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+int main(int argc, char *argv[])
+{
+ return NSApplicationMain(argc, (const char **)argv);
+}