#import "RoutingHTTPServer.h" #import "RoutingConnection.h" #import "Route.h" #pragma clang diagnostic ignored "-Wdirect-ivar-access" #pragma clang diagnostic ignored "-Widiomatic-parentheses" @implementation RoutingHTTPServer { NSMutableDictionary *routes; NSMutableDictionary *defaultHeaders; NSMutableDictionary *mimeTypes; dispatch_queue_t routeQueue; } @synthesize defaultHeaders; - (id)init { if (self = [super init]) { connectionClass = [RoutingConnection self]; routes = [[NSMutableDictionary alloc] init]; defaultHeaders = [[NSMutableDictionary alloc] init]; [self setupMIMETypes]; } return self; } #if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE - (void)dealloc { if (routeQueue) dispatch_release(routeQueue); } #endif - (void)setDefaultHeaders:(NSDictionary *)headers { if (headers) { defaultHeaders = [headers mutableCopy]; } else { defaultHeaders = [[NSMutableDictionary alloc] init]; } } - (void)setDefaultHeader:(NSString *)field value:(NSString *)value { [defaultHeaders setObject:value forKey:field]; } - (dispatch_queue_t)routeQueue { return routeQueue; } - (void)setRouteQueue:(dispatch_queue_t)queue { #if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE if (queue) dispatch_retain(queue); if (routeQueue) dispatch_release(routeQueue); #endif routeQueue = queue; } - (NSDictionary *)mimeTypes { return mimeTypes; } - (void)setMIMETypes:(NSDictionary *)types { NSMutableDictionary *newTypes; if (types) { newTypes = [types mutableCopy]; } else { newTypes = [[NSMutableDictionary alloc] init]; } mimeTypes = newTypes; } - (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext { [mimeTypes setObject:theType forKey:ext]; } - (NSString *)mimeTypeForPath:(NSString *)path { NSString *ext = [[path pathExtension] lowercaseString]; if (!ext || [ext length] < 1) return nil; return [mimeTypes objectForKey:ext]; } - (void)get:(NSString *)path withBlock:(RequestHandler)block { [self handleMethod:@"GET" withPath:path block:block]; } - (void)post:(NSString *)path withBlock:(RequestHandler)block { [self handleMethod:@"POST" withPath:path block:block]; } - (void)put:(NSString *)path withBlock:(RequestHandler)block { [self handleMethod:@"PUT" withPath:path block:block]; } - (void)delete:(NSString *)path withBlock:(RequestHandler)block { [self handleMethod:@"DELETE" withPath:path block:block]; } - (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block { Route *route = [self routeWithPath:path]; route.handler = block; [self addRoute:route forMethod:method]; } - (void)handleMethod:(NSString *)method withPath:(NSString *)path target:(id)target selector:(SEL)selector { Route *route = [self routeWithPath:path]; route.target = target; route.selector = selector; [self addRoute:route forMethod:method]; } - (void)addRoute:(Route *)route forMethod:(NSString *)method { method = [method uppercaseString]; NSMutableArray *methodRoutes = [routes objectForKey:method]; if (methodRoutes == nil) { methodRoutes = [NSMutableArray array]; [routes setObject:methodRoutes forKey:method]; } [methodRoutes addObject:route]; // Define a HEAD route for all GET routes if ([method isEqualToString:@"GET"]) { [self addRoute:route forMethod:@"HEAD"]; } } - (Route *)routeWithPath:(NSString *)path { Route *route = [[Route alloc] init]; NSMutableArray *keys = [NSMutableArray array]; if ([path length] > 2 && [path characterAtIndex:0] == '{') { // This is a custom regular expression, just remove the {} path = [path substringWithRange:NSMakeRange(1, [path length] - 2)]; } else { NSRegularExpression *regex = nil; // Escape regex characters regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil]; path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; // Parse any :parameters and * in the path regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" options:0 error:nil]; NSMutableString *regexPath = [NSMutableString stringWithString:path]; __block NSInteger diff = 0; [regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); NSString *replacementString; NSString *capturedString = [path substringWithRange:result.range]; if ([capturedString isEqualToString:@"*"]) { [keys addObject:@"wildcards"]; replacementString = @"(.*?)"; } else { NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]]; [keys addObject:keyString]; replacementString = @"([^/]+)"; } [regexPath replaceCharactersInRange:replacementRange withString:replacementString]; diff += replacementString.length - result.range.length; }]; path = [NSString stringWithFormat:@"^%@$", regexPath]; } route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil]; if ([keys count] > 0) { route.keys = keys; } return route; } - (BOOL)supportsMethod:(NSString *)method { return ([routes objectForKey:method] != nil); } - (void)handleRoute:(Route *)route withRequest:(RouteRequest *)request response:(RouteResponse *)response { if (route.handler) { route.handler(request, response); } else { id target = route.target; SEL selector = route.selector; NSMethodSignature *signature = [target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:selector]; [invocation setArgument:&request atIndex:2]; [invocation setArgument:&response atIndex:3]; [invocation invokeWithTarget:target]; } } - (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)httpMessage connection:(HTTPConnection *)connection { NSMutableArray *methodRoutes = [routes objectForKey:method]; if (methodRoutes == nil) return nil; for (Route *route in methodRoutes) { NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)]; if (!result) continue; // The first range is all of the text matched by the regex. NSUInteger captureCount = [result numberOfRanges]; if (route.keys) { // Add the route's parameters to the parameter dictionary, accounting for // the first range containing the matched text. if (captureCount == [route.keys count] + 1) { NSMutableDictionary *newParams = [params mutableCopy]; NSUInteger index = 1; BOOL firstWildcard = YES; for (NSString *key in route.keys) { NSString *capture = [path substringWithRange:[result rangeAtIndex:index]]; if ([key isEqualToString:@"wildcards"]) { NSMutableArray *wildcards = [newParams objectForKey:key]; if (firstWildcard) { // Create a new array and replace any existing object with the same key wildcards = [NSMutableArray array]; [newParams setObject:wildcards forKey:key]; firstWildcard = NO; } [wildcards addObject:capture]; } else { [newParams setObject:capture forKey:key]; } index++; } params = newParams; } } else if (captureCount > 1) { // For custom regular expressions place the anonymous captures in the captures parameter NSMutableDictionary *newParams = [params mutableCopy]; NSMutableArray *captures = [NSMutableArray array]; for (NSUInteger i = 1; i < captureCount; i++) { [captures addObject:[path substringWithRange:[result rangeAtIndex:i]]]; } [newParams setObject:captures forKey:@"captures"]; params = newParams; } RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params]; RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection]; if (!routeQueue) { [self handleRoute:route withRequest:request response:response]; } else { // Process the route on the specified queue dispatch_sync(routeQueue, ^{ @autoreleasepool { [self handleRoute:route withRequest:request response:response]; } }); } return response; } return nil; } - (void)setupMIMETypes { mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys: @"application/x-javascript", @"js", @"image/gif", @"gif", @"image/jpeg", @"jpg", @"image/jpeg", @"jpeg", @"image/png", @"png", @"image/svg+xml", @"svg", @"image/tiff", @"tif", @"image/tiff", @"tiff", @"image/x-icon", @"ico", @"image/x-ms-bmp", @"bmp", @"text/css", @"css", @"text/html", @"html", @"text/html", @"htm", @"text/plain", @"txt", @"text/xml", @"xml", nil]; } @end