Open source FBPortForwarding

Summary:FBPortForwarding is incomplete and flaky implementation of `adb reverse`
functionality for iOS. We haven't worked on it internally for a while now,
but I think it might be a very usefull tool for the community. Some external
contributors (hello philikon!) expressed interest in maintaining it.

This diff moves it over to OSS RN `Libraries` folder.

Reviewed By: vjeux

Differential Revision: D3056293

fb-gh-sync-id: 9ced61fa8480a923771bba26fe09337c344ab3b3
shipit-source-id: 9ced61fa8480a923771bba26fe09337c344ab3b3
This commit is contained in:
Alex Kotliarskyi 2016-03-24 10:50:18 -07:00 committed by Facebook Github Bot 2
parent aec90bb46b
commit c4699d8b73
18 changed files with 1204 additions and 0 deletions

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Cocoa/Cocoa.h>
#import <FBPortForwarding-Mac/FBPortForwardingClient.h>
int main(int argc, char *argv[])
{
FBPortForwardingClient *client = [FBPortForwardingClient new];
[client forwardConnectionsToPort:8081];
[client connectToMultiplexingChannelOnPort:8025];
[[NSRunLoop currentRunLoop] run];
client = nil;
return 0;
}

View File

@ -0,0 +1,36 @@
<?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>CFBundleDisplayName</key>
<string>Port Forwarding</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.facebook.example.PortForwarding</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.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <UIKit/UIKit.h>
@interface PFAppDelegate : UIResponder <UIApplicationDelegate, NSURLConnectionDataDelegate>
@property (strong, nonatomic) UIWindow *window;
@end

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "PFAppDelegate.h"
#import <FBPortForwarding-iOS/FBPortForwardingServer.h>
@implementation PFAppDelegate
{
FBPortForwardingServer *_portForwardingServer;
}
@synthesize window = window_;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
CGRect rect = [[UIScreen mainScreen] bounds];
UIView *view = [[UIView alloc] initWithFrame:rect];
view.backgroundColor = [UIColor whiteColor];
UIViewController *controller = [UIViewController new];
controller.view = view;
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button setTitle:@"Send request" forState:UIControlStateNormal];
[button addTarget:self action:@selector(sendRequest) forControlEvents:UIControlEventTouchUpInside];
button.frame = CGRectMake(0, 0, 200, 50);
button.center = view.center;
[view addSubview:button];
self.window = [[UIWindow alloc] initWithFrame:rect];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
_portForwardingServer = [FBPortForwardingServer new];
[_portForwardingServer forwardConnectionsFromPort:8082];
[_portForwardingServer listenForMultiplexingChannelOnPort:8025];
return YES;
}
- (void)sendRequest
{
NSURLRequest *req = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://localhost:8082/404"]];
[[[NSURLConnection alloc] initWithRequest:req delegate:self] start];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Success: %@", content);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"Error: %@", error);
}
@end

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <UIKit/UIKit.h>
#import "PFAppDelegate.h"
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([PFAppDelegate class]));
}
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
@interface FBPortForwardingClient : NSObject
- (instancetype)init;
- (void)forwardConnectionsToPort:(NSUInteger)port;
- (void)connectToMultiplexingChannelOnPort:(NSUInteger)port;
- (void)close;
@end

View File

@ -0,0 +1,284 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "FBPortForwardingClient.h"
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
#import <Peertalk/PTChannel.h>
#import <Peertalk/PTUSBHub.h>
#import "FBPortForwardingCommon.h"
static const NSTimeInterval ReconnectDelay = 1.0;
@interface FBPortForwardingClient () <GCDAsyncSocketDelegate, PTChannelDelegate>
{
NSUInteger _destPort;
NSUInteger _channelPort;
NSNumber *_connectingToDeviceID;
NSNumber *_connectedDeviceID;
NSDictionary *_connectedDeviceProperties;
BOOL _notConnectedQueueSuspended;
PTChannel *_connectedChannel;
dispatch_queue_t _notConnectedQueue;
dispatch_queue_t _clientSocketsQueue;
NSMutableDictionary *_clientSockets;
}
@property (atomic, readonly) NSNumber *connectedDeviceID;
@property (atomic, assign) PTChannel *connectedChannel;
@end
@implementation FBPortForwardingClient
@synthesize connectedDeviceID = _connectedDeviceID;
- (instancetype)init
{
if (self = [super init]) {
_notConnectedQueue = dispatch_queue_create("FBPortForwarding.notConnectedQueue", DISPATCH_QUEUE_SERIAL);
_clientSocketsQueue = dispatch_queue_create("FBPortForwarding.clients", DISPATCH_QUEUE_SERIAL);
_clientSockets = [NSMutableDictionary dictionary];
}
return self;
}
- (void)forwardConnectionsToPort:(NSUInteger)port
{
_destPort = port;
}
- (void)connectToMultiplexingChannelOnPort:(NSUInteger)port
{
_channelPort = port;
[self startListeningForDevices];
[self enqueueConnectToLocalIPv4Port];
}
- (void)close
{
[self.connectedChannel close];
}
- (PTChannel *)connectedChannel {
return _connectedChannel;
}
- (void)setConnectedChannel:(PTChannel *)connectedChannel {
_connectedChannel = connectedChannel;
if (!_connectedChannel) {
for (GCDAsyncSocket *sock in [_clientSockets objectEnumerator]) {
[sock setDelegate:nil];
[sock disconnect];
}
[_clientSockets removeAllObjects];
}
// Toggle the notConnectedQueue_ depending on if we are connected or not
if (!_connectedChannel && _notConnectedQueueSuspended) {
dispatch_resume(_notConnectedQueue);
_notConnectedQueueSuspended = NO;
} else if (_connectedChannel && !_notConnectedQueueSuspended) {
dispatch_suspend(_notConnectedQueue);
_notConnectedQueueSuspended = YES;
}
if (!_connectedChannel && _connectingToDeviceID) {
[self enqueueConnectToUSBDevice];
}
}
#pragma mark - PTChannelDelegate
- (void)ioFrameChannel:(PTChannel *)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(PTData *)payload {
//NSLog(@"received %@, %u, %u, %@", channel, type, tag, payload);
if (type == FBPortForwardingFrameTypeOpenPipe) {
GCDAsyncSocket *sock = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_clientSocketsQueue];
sock.userData = @(tag);
_clientSockets[@(tag)] = sock;
NSError *connectError;
if (![sock connectToHost:@"localhost" onPort:_destPort error:&connectError]) {
FBPFLog(@"Failed to connect to local %lu - %@", (unsigned long)_destPort, connectError);
}
FBPFTrace(@"open socket (%d)", tag);
}
if (type == FBPortForwardingFrameTypeWriteToPipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock writeData:[NSData dataWithBytes:payload.data length:payload.length] withTimeout:-1 tag:0];
FBPFTrace(@"channel -> socket (%d) %zu bytes", tag, payload.length);
}
if (type == FBPortForwardingFrameTypeClosePipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock disconnectAfterWriting];
FBPFTrace(@"close socket (%d)", tag);
}
}
- (void)ioFrameChannel:(PTChannel *)channel didEndWithError:(NSError *)error {
if (_connectedDeviceID && [_connectedDeviceID isEqualToNumber:channel.userInfo]) {
[self didDisconnectFromDevice:_connectedDeviceID];
}
if (_connectedChannel == channel) {
FBPFTrace(@"Disconnected from %@", channel.userInfo);
self.connectedChannel = nil;
}
}
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
FBPFTrace(@"socket (%ld) connected to %@", (long)[sock.userData integerValue], host);
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
UInt32 tag = [sock.userData unsignedIntValue];
[_clientSockets removeObjectForKey:@(tag)];
FBPFTrace(@"socket (%d) disconnected", (unsigned int)tag);
[_connectedChannel sendFrameOfType:FBPortForwardingFrameTypeClosePipe tag:tag withPayload:nil callback:nil];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)_
{
UInt32 tag = [sock.userData unsignedIntValue];
[_connectedChannel sendFrameOfType:FBPortForwardingFrameTypeWriteToPipe tag:tag withPayload:NSDataToGCDData(data) callback:^(NSError *error) {
FBPFTrace(@"channel -> socket (%d), %lu bytes", (unsigned int)tag, (unsigned long)data.length);
[sock readDataWithTimeout:-1 tag:0];
}];
}
#pragma mark - Wired device connections
- (void)startListeningForDevices {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserverForName:PTUSBDeviceDidAttachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) {
NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"];
//NSLog(@"PTUSBDeviceDidAttachNotification: %@", note.userInfo);
FBPFTrace(@"PTUSBDeviceDidAttachNotification: %@", deviceID);
dispatch_async(_notConnectedQueue, ^{
if (!_connectingToDeviceID || ![deviceID isEqualToNumber:_connectingToDeviceID]) {
[self disconnectFromCurrentChannel];
_connectingToDeviceID = deviceID;
_connectedDeviceProperties = [note.userInfo objectForKey:@"Properties"];
[self enqueueConnectToUSBDevice];
}
});
}];
[nc addObserverForName:PTUSBDeviceDidDetachNotification object:PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) {
NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"];
//NSLog(@"PTUSBDeviceDidDetachNotification: %@", note.userInfo);
FBPFTrace(@"PTUSBDeviceDidDetachNotification: %@", deviceID);
if ([_connectingToDeviceID isEqualToNumber:deviceID]) {
_connectedDeviceProperties = nil;
_connectingToDeviceID = nil;
if (_connectedChannel) {
[_connectedChannel close];
}
}
}];
}
- (void)didDisconnectFromDevice:(NSNumber *)deviceID {
FBPFLog(@"Disconnected from device #%@", deviceID);
if ([_connectedDeviceID isEqualToNumber:deviceID]) {
[self willChangeValueForKey:@"connectedDeviceID"];
_connectedDeviceID = nil;
[self didChangeValueForKey:@"connectedDeviceID"];
}
}
- (void)disconnectFromCurrentChannel {
if (_connectedDeviceID && _connectedChannel) {
[_connectedChannel close];
self.connectedChannel = nil;
}
}
- (void)enqueueConnectToLocalIPv4Port {
dispatch_async(_notConnectedQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self connectToLocalIPv4Port];
});
});
}
- (void)connectToLocalIPv4Port {
PTChannel *channel = [PTChannel channelWithDelegate:self];
channel.userInfo = [NSString stringWithFormat:@"127.0.0.1:%lu", (unsigned long)_channelPort];
[channel connectToPort:_channelPort IPv4Address:INADDR_LOOPBACK callback:^(NSError *error, PTAddress *address) {
if (error) {
if (error.domain == NSPOSIXErrorDomain && (error.code == ECONNREFUSED || error.code == ETIMEDOUT)) {
// this is an expected state
} else {
FBPFTrace(@"Failed to connect to 127.0.0.1:%lu: %@", (unsigned long)_channelPort, error);
}
} else {
[self disconnectFromCurrentChannel];
self.connectedChannel = channel;
channel.userInfo = address;
FBPFLog(@"Connected to %@", address);
}
[self performSelector:@selector(enqueueConnectToLocalIPv4Port) withObject:nil afterDelay:ReconnectDelay];
}];
}
- (void)enqueueConnectToUSBDevice {
dispatch_async(_notConnectedQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self connectToUSBDevice];
});
});
}
- (void)connectToUSBDevice {
PTChannel *channel = [PTChannel channelWithDelegate:self];
channel.userInfo = _connectingToDeviceID;
channel.delegate = self;
[channel connectToPort:(int)_channelPort overUSBHub:PTUSBHub.sharedHub deviceID:_connectingToDeviceID callback:^(NSError *error) {
if (error) {
FBPFTrace(@"Failed to connect to device #%@: %@", channel.userInfo, error);
if (channel.userInfo == _connectingToDeviceID) {
[self performSelector:@selector(enqueueConnectToUSBDevice) withObject:nil afterDelay:ReconnectDelay];
}
} else {
_connectedDeviceID = _connectingToDeviceID;
self.connectedChannel = channel;
FBPFLog(@"Connected to device #%@\n%@", _connectingToDeviceID, _connectedDeviceProperties);
}
}];
}
@end

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#define FBPFTrace(...) /*NSLog(__VA_ARGS__)*/
#define FBPFLog(...) NSLog(__VA_ARGS__)
enum {
FBPortForwardingFrameTypeOpenPipe = 201,
FBPortForwardingFrameTypeWriteToPipe = 202,
FBPortForwardingFrameTypeClosePipe = 203,
};
static dispatch_data_t NSDataToGCDData(NSData *data) {
__block NSData *retainedData = data;
return dispatch_data_create(data.bytes, data.length, nil, ^{
retainedData = nil;
});
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
@interface FBPortForwardingServer : NSObject
- (instancetype)init;
- (void)listenForMultiplexingChannelOnPort:(NSUInteger)port;
- (void)forwardConnectionsFromPort:(NSUInteger)port;
- (void)close;
@end

View File

@ -0,0 +1,193 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "FBPortForwardingServer.h"
#import <UIKit/UIKit.h>
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
#import <Peertalk/PTChannel.h>
#import "FBPortForwardingCommon.h"
@interface FBPortForwardingServer () <PTChannelDelegate, GCDAsyncSocketDelegate>
{
__weak PTChannel *_serverChannel;
__weak PTChannel *_peerChannel;
GCDAsyncSocket *_serverSocket;
NSMutableDictionary *_clientSockets;
UInt32 _lastClientSocketTag;
dispatch_queue_t _socketQueue;
PTProtocol *_protocol;
}
@end
@implementation FBPortForwardingServer
- (instancetype)init
{
if (self = [super init]) {
_socketQueue = dispatch_queue_create("FBPortForwardingServer", DISPATCH_QUEUE_SERIAL);
_lastClientSocketTag = 0;
_clientSockets = [NSMutableDictionary dictionary];
_protocol = [[PTProtocol alloc] initWithDispatchQueue:_socketQueue];
}
return self;
}
- (void)dealloc
{
[self close];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)forwardConnectionsFromPort:(NSUInteger)port
{
[self _forwardConnectionsFromPort:port reportError:YES];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
[self _forwardConnectionsFromPort:port reportError:NO];
}];
}
- (void)_forwardConnectionsFromPort:(NSUInteger)port reportError:(BOOL)shouldReportError
{
GCDAsyncSocket *serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_socketQueue];
NSError *listenError;
if ([serverSocket acceptOnPort:port error:&listenError]) {
_serverSocket = serverSocket;
} else {
if (shouldReportError) {
FBPFLog(@"Failed to listen: %@", listenError);
}
}
}
- (void)listenForMultiplexingChannelOnPort:(NSUInteger)port
{
[self _listenForMultiplexingChannelOnPort:port reportError:YES];
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification object:nil queue:nil usingBlock:^(NSNotification *note) {
[self _listenForMultiplexingChannelOnPort:port reportError:NO];
}];
}
- (void)_listenForMultiplexingChannelOnPort:(NSUInteger)port reportError:(BOOL)shouldReportError
{
PTChannel *channel = [[PTChannel alloc] initWithProtocol:_protocol delegate:self];
[channel listenOnPort:port IPv4Address:INADDR_LOOPBACK callback:^(NSError *error) {
if (error) {
if (shouldReportError) {
FBPFLog(@"Failed to listen on 127.0.0.1:%lu: %@", (unsigned long)port, error);
}
} else {
FBPFTrace(@"Listening on 127.0.0.1:%lu", (unsigned long)port);
_serverChannel = channel;
}
}];
}
- (void)close
{
if (_serverChannel) {
[_serverChannel close];
_serverChannel = nil;
}
[_serverSocket disconnect];
}
#pragma mark - PTChannelDelegate
- (void)ioFrameChannel:(PTChannel *)channel didAcceptConnection:(PTChannel *)otherChannel fromAddress:(PTAddress *)address {
// Cancel any other connection. We are FIFO, so the last connection
// established will cancel any previous connection and "take its place".
if (_peerChannel) {
[_peerChannel cancel];
}
// Weak pointer to current connection. Connection objects live by themselves
// (owned by its parent dispatch queue) until they are closed.
_peerChannel = otherChannel;
_peerChannel.userInfo = address;
FBPFTrace(@"Connected to %@", address);
}
- (void)ioFrameChannel:(PTChannel *)channel didReceiveFrameOfType:(uint32_t)type tag:(uint32_t)tag payload:(PTData *)payload {
//NSLog(@"didReceiveFrameOfType: %u, %u, %@", type, tag, payload);
if (type == FBPortForwardingFrameTypeWriteToPipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock writeData:[NSData dataWithBytes:payload.data length:payload.length] withTimeout:-1 tag:0];
FBPFTrace(@"channel -> socket (%d), %zu bytes", tag, payload.length);
}
if (type == FBPortForwardingFrameTypeClosePipe) {
GCDAsyncSocket *sock = _clientSockets[@(tag)];
[sock disconnectAfterWriting];
}
}
- (void)ioFrameChannel:(PTChannel *)channel didEndWithError:(NSError *)error {
for (GCDAsyncSocket *sock in [_clientSockets objectEnumerator]) {
[sock setDelegate:nil];
[sock disconnect];
}
[_clientSockets removeAllObjects];
FBPFTrace(@"Disconnected from %@, error = %@", channel.userInfo, error);
}
#pragma mark - GCDAsyncSocketDelegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
dispatch_block_t block = ^() {
if (!_peerChannel) {
[newSocket setDelegate:nil];
[newSocket disconnect];
}
UInt32 tag = ++_lastClientSocketTag;
newSocket.userData = @(tag);
newSocket.delegate = self;
_clientSockets[@(tag)] = newSocket;
[_peerChannel sendFrameOfType:FBPortForwardingFrameTypeOpenPipe tag:_lastClientSocketTag withPayload:nil callback:^(NSError *error) {
FBPFTrace(@"open socket (%d), error = %@", (unsigned int)tag, error);
[newSocket readDataWithTimeout:-1 tag:0];
}];
};
if (_peerChannel) {
block();
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), _socketQueue, block);
}
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)_
{
UInt32 tag = [[sock userData] unsignedIntValue];
FBPFTrace(@"Incoming data on socket (%d) - %lu bytes", (unsigned int)tag, (unsigned long)data.length);
[_peerChannel sendFrameOfType:FBPortForwardingFrameTypeWriteToPipe tag:tag withPayload:NSDataToGCDData(data) callback:^(NSError *error) {
FBPFTrace(@"socket (%d) -> channel %lu bytes, error = %@", (unsigned int)tag, (unsigned long)data.length, error);
[sock readDataWithTimeout:-1 tag:_];
}];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
UInt32 tag = [sock.userData unsignedIntValue];
[_clientSockets removeObjectForKey:@(tag)];
[_peerChannel sendFrameOfType:FBPortForwardingFrameTypeClosePipe tag:tag withPayload:nil callback:^(NSError *error) {
FBPFTrace(@"socket (%d) disconnected, err = %@, peer error = %@", (unsigned int)tag, err, error);
}];
}
@end

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
@interface PFPingClient : NSObject <GCDAsyncSocketDelegate>
- (BOOL)connectToLocalServerOnPort:(NSUInteger)port;
- (void)sendPing:(NSData *)ping;
@property (nonatomic, copy, readonly) NSArray *pongs;
@property (nonatomic, readonly) BOOL connected;
@end

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "PFPingClient.h"
@implementation PFPingClient
{
GCDAsyncSocket *_client;
}
- (instancetype)init
{
if (self = [super init]) {
_pongs = [NSArray array];
_client = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return self;
}
- (BOOL)connectToLocalServerOnPort:(NSUInteger)port
{
return [_client connectToHost:@"localhost" onPort:port error:nil];
}
- (void)sendPing:(NSData *)ping
{
[_client writeData:ping withTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
_connected = YES;
[_client readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
_pongs = [_pongs arrayByAddingObject:data];
[_client readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
_connected = NO;
}
@end

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
@interface PFPingServer : NSObject <GCDAsyncSocketDelegate>
- (instancetype)initWithPort:(NSUInteger)port;
@property (readonly, nonatomic) NSInteger clientsCount;
@end

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "PFPingServer.h"
@implementation PFPingServer
{
GCDAsyncSocket *_server;
NSMutableArray *_clients;
}
- (instancetype)initWithPort:(NSUInteger)port
{
if (self = [super init]) {
_clients = [NSMutableArray array];
_server = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *error;
if (![_server acceptOnPort:port error:&error]) {
NSLog(@"Failed to listen on port %lu: %@", (unsigned long)port, error);
return nil;
}
}
return self;
}
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
[_clients addObject:newSocket];
[newSocket readDataWithTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
[sock writeData:data withTimeout:-1 tag:0];
[sock readDataWithTimeout:-1 tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
[_clients removeObject:sock];
}
- (NSInteger)clientsCount
{
return [_clients count];
}
@end

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
@interface PFSimpleHTTPServer : NSObject <GCDAsyncSocketDelegate>
- (instancetype)initWithPort:(NSUInteger)port response:(NSData *)data;
@end

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "PFSimpleHTTPServer.h"
@implementation PFSimpleHTTPServer
{
NSData *_response;
GCDAsyncSocket *_server;
NSMutableArray *_clients;
}
- (instancetype)initWithPort:(NSUInteger)port response:(NSData *)data
{
if (self = [super init]) {
_response = [data copy];
_clients = [NSMutableArray array];
_server = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
if (![_server acceptOnPort:port error:nil]) {
return nil;
};
}
return self;
}
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
[_clients addObject:newSocket];
[newSocket readDataToData:[NSData dataWithBytes:"\r\n\r\n" length:4] withTimeout:-1 tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *headers = [NSString stringWithFormat:@"HTTP/1.1 200 OK\r\nContent-Length: %lu\r\n\r\n", (unsigned long)[_response length]];
[sock writeData:[headers dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
[sock writeData:_response withTimeout:-1 tag:0];
[sock disconnectAfterWriting];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
[_clients removeObject:sock];
}
@end

View File

@ -0,0 +1,217 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <FBPortForwarding-iOS/FBPortForwardingClient.h>
#import <FBPortForwarding-iOS/FBPortForwardingServer.h>
#import <FBTest/FBTestRunLoopRunning.h>
#import "PFPingClient.h"
#import "PFPingServer.h"
#import "PFSimpleHTTPServer.h"
#define PFWaitForPongNumber(n) \
XCTAssertTrue(FBRunRunLoopUntilBlockIsTrue(^BOOL{ \
return [client1.pongs count] == n; \
}), @"Failed to receive pong");
@interface PFTests : XCTestCase <GCDAsyncSocketDelegate>
@end
@implementation PFTests
- (void)simpleHTTPTestWithData:(NSData *)data
{
FBPortForwardingServer *portForwardingServer = [[FBPortForwardingServer alloc] init];
[portForwardingServer forwardConnectionsFromPort:9701];
[portForwardingServer listenForMultiplexingChannelOnPort:8055];
FBPortForwardingClient *portForwardingClient = [[FBPortForwardingClient alloc] init];
[portForwardingClient forwardConnectionsToPort:9702];
[portForwardingClient connectToMultiplexingChannelOnPort:8055];
PFSimpleHTTPServer *httpServer = [[PFSimpleHTTPServer alloc] initWithPort:9702 response:data];
XCTAssertNotNil(httpServer);
__block BOOL finished = NO;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:9701/"]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *connectionError) {
XCTAssertNil(connectionError);
XCTAssertTrue([data isEqualToData:responseData]);
finished = YES;
}];
XCTAssert(FBRunRunLoopWithConditionReturningPassed(&finished));
[portForwardingServer close];
FBRunRunLoopBarrier();
}
- (void)testProxiesHTTPRequests
{
[self simpleHTTPTestWithData:[@"OK" dataUsingEncoding:NSUTF8StringEncoding]];
}
- (void)testLargeHTTPResponse
{
NSMutableData *largePayload = [NSMutableData data];
[largePayload setLength:10000000];
char *bytes = [largePayload mutableBytes];
for (NSUInteger i = 0; i < largePayload.length; i++) {
bytes[i] = (char)(i % 255);
}
[self simpleHTTPTestWithData:largePayload];
}
- (void)testPingPong
{
FBPortForwardingServer *portForwardingServer = [[FBPortForwardingServer alloc] init];
[portForwardingServer forwardConnectionsFromPort:9706];
[portForwardingServer listenForMultiplexingChannelOnPort:8055];
FBPortForwardingClient *portForwardingClient = [[FBPortForwardingClient alloc] init];
[portForwardingClient forwardConnectionsToPort:9705];
[portForwardingClient connectToMultiplexingChannelOnPort:8055];
PFPingServer *server = [[PFPingServer alloc] initWithPort:9705];
XCTAssertNotNil(server);
PFPingClient *client1 = [[PFPingClient alloc] init];
[client1 connectToLocalServerOnPort:9706];
PFPingClient *client2 = [[PFPingClient alloc] init];
[client2 connectToLocalServerOnPort:9706];
XCTAssertTrue(FBRunRunLoopUntilBlockIsTrue(^BOOL{
return client1.connected && client2.connected;
}), @"Failed to connect");
NSData *ping = [NSData dataWithBytes:"PING" length:4];
[client1 sendPing:ping];
PFWaitForPongNumber(1);
NSData *pong = client1.pongs[0];
XCTAssert([ping isEqualToData:pong]);
[client1 sendPing:ping];
PFWaitForPongNumber(2);
[client1 sendPing:ping];
PFWaitForPongNumber(3);
[client1 sendPing:ping];
PFWaitForPongNumber(4);
[client1 sendPing:ping];
[client1 sendPing:ping];
PFWaitForPongNumber(6);
XCTAssertEqual(0, client2.pongs.count);
[portForwardingServer close];
}
- (void)testDisconnectsWhenNoChannel
{
FBPortForwardingServer *portForwardingServer = [[FBPortForwardingServer alloc] init];
[portForwardingServer forwardConnectionsFromPort:9707];
[portForwardingServer listenForMultiplexingChannelOnPort:8056];
__block BOOL finished = NO;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:9707/"]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *connectionError) {
XCTAssertNotNil(connectionError);
finished = YES;
}];
NSDate *start = [NSDate date];
XCTAssert(FBRunRunLoopWithConditionReturningPassed(&finished));
NSTimeInterval elapsed = -[start timeIntervalSinceNow];
XCTAssertLessThan(elapsed, 5, @"Must disconnect - no port forwarding client");
[portForwardingServer close];
}
- (void)testWaitsForChannel
{
FBPortForwardingServer *portForwardingServer = [[FBPortForwardingServer alloc] init];
[portForwardingServer forwardConnectionsFromPort:9707];
[portForwardingServer listenForMultiplexingChannelOnPort:8056];
NSData *data = [@"OK" dataUsingEncoding:NSUTF8StringEncoding];
PFSimpleHTTPServer *httpServer = [[PFSimpleHTTPServer alloc] initWithPort:9702 response:data];
XCTAssertNotNil(httpServer);
__block BOOL finished = NO;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:9707/"]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *responseData, NSError *connectionError) {
XCTAssertNil(connectionError);
XCTAssertNotNil(responseData);
NSString *res = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
XCTAssertEqualObjects(res, @"OK");
finished = YES;
}];
NSDate *start = [NSDate date];
FBRunRunLoopUntilBlockIsTrue(^BOOL{
return [start timeIntervalSinceNow] < -0.5;
});
// NOTE: Establishing port forwarding connection *after* sending HTTP request
FBPortForwardingClient *portForwardingClient = [[FBPortForwardingClient alloc] init];
[portForwardingClient forwardConnectionsToPort:9702];
[portForwardingClient connectToMultiplexingChannelOnPort:8056];
XCTAssert(FBRunRunLoopWithConditionReturningPassed(&finished));
[portForwardingServer close];
}
- (void)testDisconnectsWhenChannelConnectionLost
{
FBPortForwardingServer *portForwardingServer = [[FBPortForwardingServer alloc] init];
[portForwardingServer forwardConnectionsFromPort:9706];
[portForwardingServer listenForMultiplexingChannelOnPort:8055];
FBPortForwardingClient *portForwardingClient = [[FBPortForwardingClient alloc] init];
[portForwardingClient forwardConnectionsToPort:9705];
[portForwardingClient connectToMultiplexingChannelOnPort:8055];
PFPingServer *server = [[PFPingServer alloc] initWithPort:9705];
XCTAssertNotNil(server);
PFPingClient *client1 = [[PFPingClient alloc] init];
[client1 connectToLocalServerOnPort:9706];
XCTAssertTrue(FBRunRunLoopUntilBlockIsTrue(^BOOL{
return client1.connected;
}), @"Failed to connect");
[portForwardingClient close];
XCTAssertTrue(FBRunRunLoopUntilBlockIsTrue(^BOOL{
return !client1.connected;
}), @"Failed to disconnect");
XCTAssertEqual(server.clientsCount, 0);
[portForwardingServer close];
}
@end

View File

@ -0,0 +1,66 @@
# FBPortForwarding
FBPortForwarding lets you expose your Mac's port to iOS device via lightning
cable. The typical usecase is connecting to a TCP server that runs on OS X
from an iPhone app without common WiFi network.
## Benefits:
1. No need to be on the same WiFi, worry about firewalls (fbguest) or VPN
2. iOS app doesn't have to know your Mac's IP address
3. Secure - communication is possible only when connected via USB
## How it works
iOS provides a way to connect to device's TCP server from Mac via USBHub, but
there is no API to connect from iOS to TCP server running on Mac. FBPortForwarding
uses [Peertalk](https://github.com/rsms/peertalk) to establish communication
channel from Mac to iOS, creates a TCP server on iOS and multiplexes all
connections to that server via the peertalk channel. Helper app running on Mac
listens for commands on the peertalk channel and initializes TCP connections
to local port and forwards all communication back via the same peertalk channel.
|
iOS Device | Mac
|
+----------------+ +----------------+
|Peertalk Server | connect |Peertalk Client |
| <------------+ |
| | | |
| Port 8025| | |
+----+-----------+ +---------^------+
| |
| |
incoming +----------------+ | | +--------------+
connections |Proxy Server | | | |Real Server |
------------->> | | +-------------+ commands | | |
| Port 8081| | create | | stream | | Port 8081|
+-+--------------+ +---------> Peertalk <----------+ +-^------------+
| | Channel | ^
| +--------+ | | +--------+ | outgoing
| | | onConnect | | connect | | | connections
+---> Client +---------------> OpenPipe +---------------> Client +-----+
| #[tag] | onRead | | write | #[tag] |
| +---------------> WriteToPipe +---------------> |
| | onDisconnect | | disconnect | |
| +---------------> ClosePipe +---------------> |
| | | | | |
| | write | | onRead | |
| <---------------+ WriteToPipe <---------------+ |
| | close | | onDisconnect | |
| <---------------+ ClosePipe <---------------+ |
| | | | | |
+--------+ | | +--------+
+-------------+
First, the library on iOS device creates a TCP server on the port we want to
forward (let's say 8081) and a special Peertalk server on port 8025. Mac helper
app looks for connected iOS devices, and once it finds one it connects to its
peertalk server. Only *one* channel is created that's going to be used for
multiplexing data.
When a socket connects to local proxy server, FBPortForwarding is going to assign
a tag to the connection and use peertalk channel to tell Mac helper app to connect
to TCP port 8081 on Mac. Now events and data on both sides of the wire are going
to be multiplexed and transferred via the peertalk channel.