/* * libjingle * Copyright 2013, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "APPRTCAppClient.h" #import #import "GAEChannelClient.h" #import "RTCIceServer.h" @interface APPRTCAppClient () @property(nonatomic, strong) dispatch_queue_t backgroundQueue; @property(nonatomic, copy) NSString *baseURL; @property(nonatomic, strong) GAEChannelClient *gaeChannel; @property(nonatomic, copy) NSString *postMessageUrl; @property(nonatomic, copy) NSString *pcConfig; @property(nonatomic, strong) NSMutableString *receivedData; @property(atomic, strong) NSMutableArray *sendQueue; @property(nonatomic, copy) NSString *token; @property(nonatomic, assign) BOOL verboseLogging; @end @implementation APPRTCAppClient - (id)init { if (self = [super init]) { _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue", NULL); _sendQueue = [NSMutableArray array]; // Uncomment to see Request/Response logging. //_verboseLogging = YES; } return self; } #pragma mark - Public methods - (void)connectToRoom:(NSURL *)url { NSURLRequest *request = [self getRequestFromUrl:url]; [NSURLConnection connectionWithRequest:request delegate:self]; } - (void)sendData:(NSData *)data { @synchronized(self) { [self maybeLogMessage:@"Send message"]; [self.sendQueue addObject:[data copy]]; } [self requestQueueDrainInBackground]; } #pragma mark - Internal methods - (NSTextCheckingResult *)findMatch:(NSString *)regexpPattern withString:(NSString *)string errorMessage:(NSString *)errorMessage { NSError *error; NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:regexpPattern options:0 error:&error]; if (error) { [self maybeLogMessage: [NSString stringWithFormat:@"Failed to create regexp - %@", [error description]]]; return nil; } NSRange fullRange = NSMakeRange(0, [string length]); NSArray *matches = [regexp matchesInString:string options:0 range:fullRange]; if ([matches count] == 0) { if ([errorMessage length] > 0) { [self maybeLogMessage:string]; [self showMessage: [NSString stringWithFormat:@"Missing %@ in HTML.", errorMessage]]; } return nil; } else if ([matches count] > 1) { if ([errorMessage length] > 0) { [self maybeLogMessage:string]; [self showMessage:[NSString stringWithFormat:@"Too many %@s in HTML.", errorMessage]]; } return nil; } return matches[0]; } - (NSURLRequest *)getRequestFromUrl:(NSURL *)url { self.receivedData = [NSMutableString stringWithCapacity:20000]; NSString *path = [NSString stringWithFormat:@"https:%@", [url resourceSpecifier]]; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:path]]; return request; } - (void)maybeLogMessage:(NSString *)message { if (self.verboseLogging) { NSLog(@"%@", message); } } - (void)requestQueueDrainInBackground { dispatch_async(self.backgroundQueue, ^(void) { // TODO(hughv): This can block the UI thread. Fix. @synchronized(self) { if ([self.postMessageUrl length] < 1) { return; } for (NSData *data in self.sendQueue) { NSString *url = [NSString stringWithFormat:@"%@/%@", self.baseURL, self.postMessageUrl]; [self sendData:data withUrl:url]; } [self.sendQueue removeAllObjects]; } }); } - (void)sendData:(NSData *)data withUrl:(NSString *)url { NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; request.HTTPMethod = @"POST"; [request setHTTPBody:data]; NSURLResponse *response; NSError *error; NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; int status = [httpResponse statusCode]; NSAssert(status == 200, @"Bad response [%d] to message: %@\n\n%@", status, [NSString stringWithUTF8String:[data bytes]], [NSString stringWithUTF8String:[responseData bytes]]); } - (void)showMessage:(NSString *)message { NSLog(@"%@", message); UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Unable to join" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } - (void)updateIceServers:(NSMutableArray *)iceServers withTurnServer:(NSString *)turnServerUrl { if ([turnServerUrl length] < 1) { [self.iceServerDelegate onIceServers:iceServers]; return; } dispatch_async(self.backgroundQueue, ^(void) { NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:turnServerUrl]]; [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; [request addValue:@"https://apprtc.appspot.com" forHTTPHeaderField:@"origin"]; NSURLResponse *response; NSError *error; NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if (!error) { NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error]; NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); NSString *username = json[@"username"]; NSString *turnServer = json[@"turn"]; NSString *password = json[@"password"]; NSString *fullUrl = [NSString stringWithFormat:@"turn:%@@%@", username, turnServer]; RTCIceServer *iceServer = [[RTCIceServer alloc] initWithUri:[NSURL URLWithString:fullUrl] password:password]; [iceServers addObject:iceServer]; } else { NSLog(@"Unable to get TURN server. Error: %@", error.description); } dispatch_async(dispatch_get_main_queue(), ^(void) { [self.iceServerDelegate onIceServers:iceServers]; }); }); } #pragma mark - NSURLConnectionDataDelegate methods - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSString *roomHtml = [NSString stringWithUTF8String:[data bytes]]; [self maybeLogMessage: [NSString stringWithFormat:@"Received %d chars", [roomHtml length]]]; [self.receivedData appendString:roomHtml]; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; int statusCode = [httpResponse statusCode]; [self maybeLogMessage: [NSString stringWithFormat: @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@", [httpResponse URL], statusCode, [httpResponse allHeaderFields]]]; NSAssert(statusCode == 200, @"Invalid response of %d received.", statusCode); } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self maybeLogMessage:[NSString stringWithFormat:@"finished loading %d chars", [self.receivedData length]]]; NSTextCheckingResult *result = [self findMatch:@".*\n *Sorry, this room is full\\..*" withString:self.receivedData errorMessage:nil]; if (result) { [self showMessage:@"Room full"]; return; } NSString *fullUrl = [[[connection originalRequest] URL] absoluteString]; NSRange queryRange = [fullUrl rangeOfString:@"?"]; self.baseURL = [fullUrl substringToIndex:queryRange.location]; [self maybeLogMessage:[NSString stringWithFormat:@"URL\n%@", self.baseURL]]; result = [self findMatch:@".*\n *openChannel\\('([^']*)'\\);\n.*" withString:self.receivedData errorMessage:@"channel token"]; if (!result) { return; } self.token = [self.receivedData substringWithRange:[result rangeAtIndex:1]]; [self maybeLogMessage:[NSString stringWithFormat:@"Token\n%@", self.token]]; result = [self findMatch:@".*\n *path = '/(message\\?r=.+)' \\+ '(&u=[0-9]+)';\n.*" withString:self.receivedData errorMessage:@"postMessage URL"]; if (!result) { return; } self.postMessageUrl = [NSString stringWithFormat:@"%@%@", [self.receivedData substringWithRange:[result rangeAtIndex:1]], [self.receivedData substringWithRange:[result rangeAtIndex:2]]]; [self maybeLogMessage:[NSString stringWithFormat:@"POST message URL\n%@", self.postMessageUrl]]; result = [self findMatch:@".*\n *var pc_config = (\\{[^\n]*\\});\n.*" withString:self.receivedData errorMessage:@"pc_config"]; if (!result) { return; } NSString *pcConfig = [self.receivedData substringWithRange:[result rangeAtIndex:1]]; [self maybeLogMessage: [NSString stringWithFormat:@"PC Config JSON\n%@", pcConfig]]; result = [self findMatch:@".*\n *requestTurn\\('([^\n]*)'\\);\n.*" withString:self.receivedData errorMessage:@"channel token"]; NSString *turnServerUrl; if (result) { turnServerUrl = [self.receivedData substringWithRange:[result rangeAtIndex:1]]; [self maybeLogMessage: [NSString stringWithFormat:@"TURN server request URL\n%@", turnServerUrl]]; } NSError *error; NSData *pcData = [pcConfig dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:pcData options:0 error:&error]; NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); NSArray *servers = [json objectForKey:@"iceServers"]; NSMutableArray *iceServers = [NSMutableArray array]; for (NSDictionary *server in servers) { NSString *url = [server objectForKey:@"url"]; NSString *credential = [server objectForKey:@"credential"]; if (!credential) { credential = @""; } [self maybeLogMessage: [NSString stringWithFormat:@"url [%@] - credential [%@]", url, credential]]; RTCIceServer *iceServer = [[RTCIceServer alloc] initWithUri:[NSURL URLWithString:url] password:credential]; [iceServers addObject:iceServer]; } [self updateIceServers:iceServers withTurnServer:turnServerUrl]; [self maybeLogMessage: [NSString stringWithFormat:@"About to open GAE with token: %@", self.token]]; self.gaeChannel = [[GAEChannelClient alloc] initWithToken:self.token delegate:self.messageHandler]; } @end