1/* 2 * libjingle 3 * Copyright 2013, Google Inc. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. The name of the author may not be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28#import "APPRTCAppClient.h" 29 30#import <dispatch/dispatch.h> 31 32#import "GAEChannelClient.h" 33#import "RTCICEServer.h" 34 35@interface APPRTCAppClient () 36 37@property(nonatomic, strong) dispatch_queue_t backgroundQueue; 38@property(nonatomic, copy) NSString *baseURL; 39@property(nonatomic, strong) GAEChannelClient *gaeChannel; 40@property(nonatomic, copy) NSString *postMessageUrl; 41@property(nonatomic, copy) NSString *pcConfig; 42@property(nonatomic, strong) NSMutableString *roomHtml; 43@property(atomic, strong) NSMutableArray *sendQueue; 44@property(nonatomic, copy) NSString *token; 45 46@property(nonatomic, assign) BOOL verboseLogging; 47 48@end 49 50@implementation APPRTCAppClient 51 52@synthesize ICEServerDelegate = _ICEServerDelegate; 53@synthesize messageHandler = _messageHandler; 54 55@synthesize backgroundQueue = _backgroundQueue; 56@synthesize baseURL = _baseURL; 57@synthesize gaeChannel = _gaeChannel; 58@synthesize postMessageUrl = _postMessageUrl; 59@synthesize pcConfig = _pcConfig; 60@synthesize roomHtml = _roomHtml; 61@synthesize sendQueue = _sendQueue; 62@synthesize token = _token; 63@synthesize verboseLogging = _verboseLogging; 64 65- (id)init { 66 if (self = [super init]) { 67 _backgroundQueue = dispatch_queue_create("RTCBackgroundQueue", NULL); 68 _sendQueue = [NSMutableArray array]; 69 // Uncomment to see Request/Response logging. 70 // _verboseLogging = YES; 71 } 72 return self; 73} 74 75#pragma mark - Public methods 76 77- (void)connectToRoom:(NSURL *)url { 78 NSURLRequest *request = [self getRequestFromUrl:url]; 79 [NSURLConnection connectionWithRequest:request delegate:self]; 80} 81 82- (void)sendData:(NSData *)data { 83 @synchronized(self) { 84 [self maybeLogMessage:@"Send message"]; 85 [self.sendQueue addObject:[data copy]]; 86 } 87 [self requestQueueDrainInBackground]; 88} 89 90#pragma mark - Internal methods 91 92- (NSString*)findVar:(NSString*)name 93 strippingQuotes:(BOOL)strippingQuotes { 94 NSError* error; 95 NSString* pattern = 96 [NSString stringWithFormat:@".*\n *var %@ = ([^\n]*);\n.*", name]; 97 NSRegularExpression *regexp = 98 [NSRegularExpression regularExpressionWithPattern:pattern 99 options:0 100 error:&error]; 101 NSAssert(!error, @"Unexpected error compiling regex: ", 102 error.localizedDescription); 103 104 NSRange fullRange = NSMakeRange(0, [self.roomHtml length]); 105 NSArray *matches = 106 [regexp matchesInString:self.roomHtml options:0 range:fullRange]; 107 if ([matches count] != 1) { 108 [self showMessage:[NSString stringWithFormat:@"%d matches for %@ in %@", 109 [matches count], name, self.roomHtml]]; 110 return nil; 111 } 112 NSRange matchRange = [matches[0] rangeAtIndex:1]; 113 NSString* value = [self.roomHtml substringWithRange:matchRange]; 114 if (strippingQuotes) { 115 NSAssert([value length] > 2, 116 @"Can't strip quotes from short string: [%@]", value); 117 NSAssert(([value characterAtIndex:0] == '\'' && 118 [value characterAtIndex:[value length] - 1] == '\''), 119 @"Can't strip quotes from unquoted string: [%@]", value); 120 value = [value substringWithRange:NSMakeRange(1, [value length] - 2)]; 121 } 122 return value; 123} 124 125- (NSURLRequest *)getRequestFromUrl:(NSURL *)url { 126 self.roomHtml = [NSMutableString stringWithCapacity:20000]; 127 NSString *path = 128 [NSString stringWithFormat:@"https:%@", [url resourceSpecifier]]; 129 NSURLRequest *request = 130 [NSURLRequest requestWithURL:[NSURL URLWithString:path]]; 131 return request; 132} 133 134- (void)maybeLogMessage:(NSString *)message { 135 if (self.verboseLogging) { 136 NSLog(@"%@", message); 137 } 138} 139 140- (void)requestQueueDrainInBackground { 141 dispatch_async(self.backgroundQueue, ^(void) { 142 // TODO(hughv): This can block the UI thread. Fix. 143 @synchronized(self) { 144 if ([self.postMessageUrl length] < 1) { 145 return; 146 } 147 for (NSData *data in self.sendQueue) { 148 NSString *url = [NSString stringWithFormat:@"%@/%@", 149 self.baseURL, 150 self.postMessageUrl]; 151 [self sendData:data withUrl:url]; 152 } 153 [self.sendQueue removeAllObjects]; 154 } 155 }); 156} 157 158- (void)sendData:(NSData *)data withUrl:(NSString *)url { 159 NSMutableURLRequest *request = 160 [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; 161 request.HTTPMethod = @"POST"; 162 [request setHTTPBody:data]; 163 NSURLResponse *response; 164 NSError *error; 165 NSData *responseData = [NSURLConnection sendSynchronousRequest:request 166 returningResponse:&response 167 error:&error]; 168 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 169 int status = [httpResponse statusCode]; 170 NSAssert(status == 200, 171 @"Bad response [%d] to message: %@\n\n%@", 172 status, 173 [NSString stringWithUTF8String:[data bytes]], 174 [NSString stringWithUTF8String:[responseData bytes]]); 175} 176 177- (void)showMessage:(NSString *)message { 178 NSLog(@"%@", message); 179 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Unable to join" 180 message:message 181 delegate:nil 182 cancelButtonTitle:@"OK" 183 otherButtonTitles:nil]; 184 [alertView show]; 185} 186 187- (void)updateICEServers:(NSMutableArray *)ICEServers 188 withTurnServer:(NSString *)turnServerUrl { 189 if ([turnServerUrl length] < 1) { 190 [self.ICEServerDelegate onICEServers:ICEServers]; 191 return; 192 } 193 dispatch_async(self.backgroundQueue, ^(void) { 194 NSMutableURLRequest *request = [NSMutableURLRequest 195 requestWithURL:[NSURL URLWithString:turnServerUrl]]; 196 [request addValue:@"Mozilla/5.0" forHTTPHeaderField:@"user-agent"]; 197 [request addValue:@"https://apprtc.appspot.com" 198 forHTTPHeaderField:@"origin"]; 199 NSURLResponse *response; 200 NSError *error; 201 NSData *responseData = [NSURLConnection sendSynchronousRequest:request 202 returningResponse:&response 203 error:&error]; 204 if (!error) { 205 NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseData 206 options:0 207 error:&error]; 208 NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); 209 NSString *username = json[@"username"]; 210 NSString *password = json[@"password"]; 211 NSArray* uris = json[@"uris"]; 212 for (int i = 0; i < [uris count]; ++i) { 213 NSString *turnServer = [uris objectAtIndex:i]; 214 RTCICEServer *ICEServer = 215 [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:turnServer] 216 username:username 217 password:password]; 218 NSLog(@"Added ICE Server: %@", ICEServer); 219 [ICEServers addObject:ICEServer]; 220 } 221 } else { 222 NSLog(@"Unable to get TURN server. Error: %@", error.description); 223 } 224 225 dispatch_async(dispatch_get_main_queue(), ^(void) { 226 [self.ICEServerDelegate onICEServers:ICEServers]; 227 }); 228 }); 229} 230 231#pragma mark - NSURLConnectionDataDelegate methods 232 233- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 234 NSString *roomHtml = [NSString stringWithUTF8String:[data bytes]]; 235 [self maybeLogMessage: 236 [NSString stringWithFormat:@"Received %d chars", [roomHtml length]]]; 237 [self.roomHtml appendString:roomHtml]; 238} 239 240- (void)connection:(NSURLConnection *)connection 241 didReceiveResponse:(NSURLResponse *)response { 242 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; 243 int statusCode = [httpResponse statusCode]; 244 [self maybeLogMessage: 245 [NSString stringWithFormat: 246 @"Response received\nURL\n%@\nStatus [%d]\nHeaders\n%@", 247 [httpResponse URL], 248 statusCode, 249 [httpResponse allHeaderFields]]]; 250 NSAssert(statusCode == 200, @"Invalid response of %d received.", statusCode); 251} 252 253- (void)connectionDidFinishLoading:(NSURLConnection *)connection { 254 [self maybeLogMessage:[NSString stringWithFormat:@"finished loading %d chars", 255 [self.roomHtml length]]]; 256 NSRegularExpression* fullRegex = 257 [NSRegularExpression regularExpressionWithPattern:@"room is full" 258 options:0 259 error:nil]; 260 if ([fullRegex 261 numberOfMatchesInString:self.roomHtml 262 options:0 263 range:NSMakeRange(0, [self.roomHtml length])]) { 264 [self showMessage:@"Room full"]; 265 return; 266 } 267 268 269 NSString *fullUrl = [[[connection originalRequest] URL] absoluteString]; 270 NSRange queryRange = [fullUrl rangeOfString:@"?"]; 271 self.baseURL = [fullUrl substringToIndex:queryRange.location]; 272 [self maybeLogMessage: 273 [NSString stringWithFormat:@"Base URL: %@", self.baseURL]]; 274 275 self.token = [self findVar:@"channelToken" strippingQuotes:YES]; 276 if (!self.token) 277 return; 278 [self maybeLogMessage:[NSString stringWithFormat:@"Token: %@", self.token]]; 279 280 NSString* roomKey = [self findVar:@"roomKey" strippingQuotes:YES]; 281 NSString* me = [self findVar:@"me" strippingQuotes:YES]; 282 if (!roomKey || !me) 283 return; 284 self.postMessageUrl = 285 [NSString stringWithFormat:@"/message?r=%@&u=%@", roomKey, me]; 286 [self maybeLogMessage:[NSString stringWithFormat:@"POST message URL: %@", 287 self.postMessageUrl]]; 288 289 NSString* pcConfig = [self findVar:@"pcConfig" strippingQuotes:NO]; 290 if (!pcConfig) 291 return; 292 [self maybeLogMessage: 293 [NSString stringWithFormat:@"PC Config JSON: %@", pcConfig]]; 294 295 NSString *turnServerUrl = [self findVar:@"turnUrl" strippingQuotes:YES]; 296 if (turnServerUrl) { 297 [self maybeLogMessage: 298 [NSString stringWithFormat:@"TURN server request URL: %@", 299 turnServerUrl]]; 300 } 301 302 NSError *error; 303 NSData *pcData = [pcConfig dataUsingEncoding:NSUTF8StringEncoding]; 304 NSDictionary *json = 305 [NSJSONSerialization JSONObjectWithData:pcData options:0 error:&error]; 306 NSAssert(!error, @"Unable to parse. %@", error.localizedDescription); 307 NSArray *servers = [json objectForKey:@"iceServers"]; 308 NSMutableArray *ICEServers = [NSMutableArray array]; 309 for (NSDictionary *server in servers) { 310 NSString *url = [server objectForKey:@"url"]; 311 NSString *username = json[@"username"]; 312 NSString *credential = [server objectForKey:@"credential"]; 313 if (!username) { 314 username = @""; 315 } 316 if (!credential) { 317 credential = @""; 318 } 319 [self maybeLogMessage: 320 [NSString stringWithFormat:@"url [%@] - credential [%@]", 321 url, 322 credential]]; 323 RTCICEServer *ICEServer = 324 [[RTCICEServer alloc] initWithURI:[NSURL URLWithString:url] 325 username:username 326 password:credential]; 327 NSLog(@"Added ICE Server: %@", ICEServer); 328 [ICEServers addObject:ICEServer]; 329 } 330 [self updateICEServers:ICEServers withTurnServer:turnServerUrl]; 331 332 [self maybeLogMessage: 333 [NSString stringWithFormat:@"About to open GAE with token: %@", 334 self.token]]; 335 self.gaeChannel = 336 [[GAEChannelClient alloc] initWithToken:self.token 337 delegate:self.messageHandler]; 338} 339 340@end 341