From 5a3ed2fb78a802df9f0360107a542eecd7f10736 Mon Sep 17 00:00:00 2001 From: hmelder Date: Thu, 11 Jan 2024 20:46:58 +0100 Subject: [PATCH] Import MicroHTTPKit and write Tests --- .../MicroHTTPKit/HKHTTPConstants.h | 16 ++ .../MicroHTTPKit/MicroHTTPKit/HKHTTPRequest.h | 43 +++ .../MicroHTTPKit/HKHTTPResponse.h | 40 +++ .../MicroHTTPKit/MicroHTTPKit/HKHTTPServer.h | 27 ++ .../MicroHTTPKit/MicroHTTPKit/HKRouter.h | 64 +++++ .../MicroHTTPKit/MicroHTTPKit/MicroHTTPKit.h | 11 + .../MicroHTTPKit/Source/HKHTTPConstants.m | 16 ++ .../Source/HKHTTPRequest+Private.h | 13 + .../Source/HKHTTPRequest+Private.m | 18 ++ Libraries/MicroHTTPKit/Source/HKHTTPRequest.m | 50 ++++ .../MicroHTTPKit/Source/HKHTTPResponse.m | 68 +++++ Libraries/MicroHTTPKit/Source/HKHTTPServer.m | 257 ++++++++++++++++++ Libraries/MicroHTTPKit/Source/HKRouter.m | 80 ++++++ Libraries/MicroHTTPKit/Tests/main.h | 10 + Libraries/MicroHTTPKit/Tests/main.m | 25 ++ Libraries/MicroHTTPKit/Tests/meson.build | 18 ++ Libraries/MicroHTTPKit/Tests/routing.m | 246 +++++++++++++++++ Libraries/MicroHTTPKit/meson.build | 103 +++++++ 18 files changed, 1105 insertions(+) create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPConstants.h create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPRequest.h create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPResponse.h create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPServer.h create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/HKRouter.h create mode 100644 Libraries/MicroHTTPKit/MicroHTTPKit/MicroHTTPKit.h create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPConstants.m create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.h create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.m create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPRequest.m create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPResponse.m create mode 100644 Libraries/MicroHTTPKit/Source/HKHTTPServer.m create mode 100644 Libraries/MicroHTTPKit/Source/HKRouter.m create mode 100644 Libraries/MicroHTTPKit/Tests/main.h create mode 100644 Libraries/MicroHTTPKit/Tests/main.m create mode 100644 Libraries/MicroHTTPKit/Tests/meson.build create mode 100644 Libraries/MicroHTTPKit/Tests/routing.m create mode 100644 Libraries/MicroHTTPKit/meson.build diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPConstants.h b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPConstants.h new file mode 100644 index 0000000..11f27ee --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPConstants.h @@ -0,0 +1,16 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +extern NSString *const HKHTTPMethodGET; +extern NSString *const HKHTTPMethodHEAD; +extern NSString *const HKHTTPMethodOptions; +extern NSString *const HKHTTPMethodPOST; +extern NSString *const HKHTTPMethodPUT; + +extern NSString *const HKHTTPHeaderContentType; +extern NSString *const HKHTTPHeaderContentApplicationJSON; diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPRequest.h b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPRequest.h new file mode 100644 index 0000000..e0056a0 --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPRequest.h @@ -0,0 +1,43 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HKHTTPRequest : NSObject { + @private + NSData *_HTTPBody; +} + +@property (readonly, copy) NSString *method; +@property (readonly, copy) NSURL *URL; +@property (readonly, copy) NSDictionary *headers; +@property (readonly, copy) NSDictionary *queryParameters; + +/** + * @brief The user info dictionary for the request. + * + * This property is initialized to an empty dictionary, which you can then use to store + * app-specific information. For example, you might use it during the processing of the + * request to store processing-related data in the middleware. + */ +@property (copy) NSDictionary *userInfo; + +- (instancetype)initWithMethod:(NSString *)method + URL:(NSURL *)URL + headers:(NSDictionary *)headers; + +- (instancetype)initWithMethod:(NSString *)method + URL:(NSURL *)URL + headers:(NSDictionary *)headers + queryParameters:(NSDictionary *)queryParameters; + +- (NSData *)HTTPBody; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPResponse.h b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPResponse.h new file mode 100644 index 0000000..169d5e1 --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPResponse.h @@ -0,0 +1,40 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HKHTTPResponse : NSObject + +@property (strong, nullable) NSData *data; +@property (assign) NSUInteger status; +@property (strong) NSDictionary *headers; + ++ (instancetype)responseWithStatus:(NSUInteger)status; ++ (instancetype)responseWithData:(NSData *)data status:(NSUInteger)status; + +- (instancetype)initWithStatus:(NSUInteger)status; + +- (instancetype)initWithData:(NSData *)data status:(NSUInteger)status; + +- (instancetype)initWithData:(NSData *)data + headers:(NSDictionary *)headers + status:(NSUInteger)status; + +@end + +@interface HKHTTPJSONResponse : HKHTTPResponse + ++ (instancetype)responseWithJSONObject:(id)JSONObject + status:(NSUInteger)status + error:(NSError **)error; + +- (instancetype)initWithJSONObject:(id)JSONObject status:(NSUInteger)status error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPServer.h b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPServer.h new file mode 100644 index 0000000..de27f78 --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/HKHTTPServer.h @@ -0,0 +1,27 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HKHTTPServer : NSObject + +@property (nonatomic, readonly) NSUInteger port; +@property (readonly) HKRouter *router; + ++ (instancetype)serverWithPort:(NSUInteger)port; + +- (instancetype)initWithPort:(NSUInteger)port; + +- (BOOL)startWithError:(NSError **)error; +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/HKRouter.h b/Libraries/MicroHTTPKit/MicroHTTPKit/HKRouter.h new file mode 100644 index 0000000..37609de --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/HKRouter.h @@ -0,0 +1,64 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import +#include + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef HKHTTPResponse *_Nonnull (^HKHandlerBlock)(HKHTTPRequest *request); + +extern NSString *const HKResponseDataKey; +extern NSString *const HKResponseStatusKey; + +@interface HKRoute : NSObject + +@property (readonly, copy) NSString *path; +@property (readonly, copy) HKHandlerBlock handler; +@property (readonly, copy) NSString *method; + ++ (instancetype)routeWithPath:(NSString *)path + method:(NSString *)method + handler:(HKHandlerBlock)handler; + +- (instancetype)initWithPath:(NSString *)path + method:(NSString *)method + handler:(HKHandlerBlock)handler; + +@end + +@interface HKRouter : NSObject + +@property (copy) HKHandlerBlock notFoundHandler; + +/** + * A middleware block that is called before the handler block. + * The middleware block can be used to modify the userInfo dictionary in the request before it is + * handled. + * + * If the middleware block returns a HKHTTPResponse object, this response is used instead of + * calling the handler block. + */ +@property (copy, nullable) HKHandlerBlock middleware; + +- (HKHandlerBlock)handlerForRequest:(HKHTTPRequest *)request; + ++ (instancetype)routerWithRoutes:(NSArray *)routes + notFoundHandler:(HKHandlerBlock)notFoundHandler; + +- (instancetype)initWithRoutes:(NSArray *)routes + notFoundHandler:(HKHandlerBlock)notFoundHandler NS_DESIGNATED_INITIALIZER; + +- (void)registerRoute:(HKRoute *)route; + +- (NSArray *)routes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/MicroHTTPKit/MicroHTTPKit/MicroHTTPKit.h b/Libraries/MicroHTTPKit/MicroHTTPKit/MicroHTTPKit.h new file mode 100644 index 0000000..048cd7a --- /dev/null +++ b/Libraries/MicroHTTPKit/MicroHTTPKit/MicroHTTPKit.h @@ -0,0 +1,11 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import +#import +#import +#import +#import diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPConstants.m b/Libraries/MicroHTTPKit/Source/HKHTTPConstants.m new file mode 100644 index 0000000..2d53df5 --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPConstants.m @@ -0,0 +1,16 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +NSString *const HKHTTPMethodGET = @"GET"; +NSString *const HKHTTPMethodHEAD = @"HEAD"; +NSString *const HKHTTPMethodOptions = @"OPTIONS"; +NSString *const HKHTTPMethodPOST = @"POST"; +NSString *const HKHTTPMethodPUT = @"PUT"; + +NSString *const HKHTTPHeaderContentType = @"Content-Type"; +NSString *const HKHTTPHeaderContentApplicationJSON = @"application/json"; diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.h b/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.h new file mode 100644 index 0000000..7bc86db --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.h @@ -0,0 +1,13 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +@interface HKHTTPRequest (Private) + +- (void)appendBytesToHTTPBody:(const void *)bytes length:(NSUInteger)length; + +@end diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.m b/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.m new file mode 100644 index 0000000..7b1ca66 --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPRequest+Private.m @@ -0,0 +1,18 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import "HKHTTPRequest+Private.h" + +@implementation HKHTTPRequest (Private) + +- (void)appendBytesToHTTPBody:(const void *)bytes length:(NSUInteger)length { + NSAssert([_HTTPBody isKindOfClass:[NSMutableData class]], @"HTTPBody is not mutable", nil); + + NSMutableData *data = (NSMutableData *) _HTTPBody; + [data appendBytes:bytes length:length]; +} + +@end diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPRequest.m b/Libraries/MicroHTTPKit/Source/HKHTTPRequest.m new file mode 100644 index 0000000..28e2b6b --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPRequest.m @@ -0,0 +1,50 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +@implementation HKHTTPRequest + +- (instancetype)initWithMethod:(NSString *)method + URL:(NSURL *)URL + headers:(NSDictionary *)headers { + return [self initWithMethod:method URL:URL headers:headers HTTPBody:nil]; +} + +// If we don't pass a HTTPBody in the initialiser, we assume that we will append data later on. +// We thus create a mutable data object. +- (instancetype)initWithMethod:(NSString *)method + URL:(NSURL *)URL + headers:(NSDictionary *)headers + queryParameters:(NSDictionary *)queryParameters { + self = [self initWithMethod:method URL:URL headers:headers]; + if (self) { + _queryParameters = [queryParameters copy]; + _HTTPBody = [NSMutableData data]; + } + return self; +} + +- (instancetype)initWithMethod:(NSString *)method + URL:(NSURL *)URL + headers:(NSDictionary *)headers + HTTPBody:(nullable NSData *)HTTPBody { + self = [super init]; + if (self) { + _method = [method copy]; + _URL = [URL copy]; + _headers = [headers copy]; + _HTTPBody = [HTTPBody copy]; + _userInfo = @{}; + } + return self; +} + +- (NSData *)HTTPBody { + return [_HTTPBody copy]; +} + +@end diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPResponse.m b/Libraries/MicroHTTPKit/Source/HKHTTPResponse.m new file mode 100644 index 0000000..6bcba46 --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPResponse.m @@ -0,0 +1,68 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import +#import + +@implementation HKHTTPResponse + ++ (instancetype)responseWithStatus:(NSUInteger)status { + return [[self alloc] initWithStatus:status]; +} + ++ (instancetype)responseWithData:(NSData *)data status:(NSUInteger)status { + return [[self alloc] initWithData:data status:status]; +} + +- (instancetype)initWithStatus:(NSUInteger)status { + self = [super init]; + + if (self) { + _status = status; + } + + return self; +} + +- (instancetype)initWithData:(NSData *)data status:(NSUInteger)status { + return [self initWithData:data headers:@{} status:status]; +} + +- (instancetype)initWithData:(NSData *)data + headers:(NSDictionary *)headers + status:(NSUInteger)status { + self = [super init]; + if (self) { + _data = data; + _headers = headers; + _status = status; + } + return self; +} + +@end + +@implementation HKHTTPJSONResponse + ++ (instancetype)responseWithJSONObject:(id)JSONObject + status:(NSUInteger)status + error:(NSError **)error { + return [[self alloc] initWithJSONObject:JSONObject status:status error:error]; +} + +- (instancetype)initWithJSONObject:(id)JSONObject + status:(NSUInteger)status + error:(NSError **)error { + NSData *data; + NSDictionary *headers; + + headers = @{HKHTTPHeaderContentType : HKHTTPHeaderContentApplicationJSON}; + data = [NSJSONSerialization dataWithJSONObject:JSONObject options:0 error:error]; + + return [self initWithData:data headers:headers status:status]; +} + +@end diff --git a/Libraries/MicroHTTPKit/Source/HKHTTPServer.m b/Libraries/MicroHTTPKit/Source/HKHTTPServer.m new file mode 100644 index 0000000..32cb172 --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKHTTPServer.m @@ -0,0 +1,257 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import +#import + +// Private headers +#import "HKHTTPRequest+Private.h" + +#include + +// Private methods for request handling +@interface HKHTTPServer (Private) +- (enum MHD_Result)_sendResponseForRequest:(HKHTTPRequest *)request + connection:(struct MHD_Connection *)conn + URL:(NSURL *)URL + method:(NSString *)method; +@end + +// Our trampoline block type for MHD_KeyValueIterator callbacks +typedef BOOL (^HKKeyValueBlock)(enum MHD_ValueKind kind, NSString *key, NSString *value); + +/* A MHD_KeyValueIterator callback that acts as a trampoline to a HKKeyValueBlock, + * and used by HKConnectionValuesFromBlock to call the HKKeyValueBlock for each key-value pair. + */ +static enum MHD_Result _keyValueTrampoline(void *blockPointer, enum MHD_ValueKind kind, + const char *key, const char *value) { + @autoreleasepool { + NSString *keyString; + NSString *valueString; + HKKeyValueBlock block; + + keyString = [NSString stringWithUTF8String:key]; + valueString = value ? [NSString stringWithUTF8String:value] : @""; + block = (__bridge HKKeyValueBlock) blockPointer; + + // Call the HKKeyValueBlock + if (block(kind, keyString, valueString)) { + return MHD_YES; + } else { + return MHD_NO; + } + } +} + +/* Wrapper around MHD_get_connection_values that calls a HKKeyValueBlock for each key-value pair. + * Returns MHD_YES if all calls to the HKKeyValueBlock returned YES, otherwise MHD_NO. + */ +int HKConnectionValuesFromBlock(struct MHD_Connection *connection, enum MHD_ValueKind kind, + HKKeyValueBlock block) { + return MHD_get_connection_values(connection, kind, _keyValueTrampoline, + (__bridge void *) (block)); +} + +/* A MHD_AccessHandlerCallback to handle new incoming requests. + * We invoke the HKHTTPServer's _handleRequestForConnection:URL:method: method to handle the + * request. + */ +static enum MHD_Result accessHandler(void *cls, struct MHD_Connection *connection, const char *url, + const char *method, + __attribute__((unused)) const char *version, + const char *upload_data, size_t *upload_data_size, + void **con_cls) { + @autoreleasepool { + NSLog(@"Received request for %s %s with body size %lu", method, url, *upload_data_size); + HKHTTPServer *server; + HKHTTPRequest *request; + + server = (__bridge HKHTTPServer *) cls; + + if (*con_cls == NULL) { + NSString *urlString; + NSString *methodString; + NSMutableDictionary *headers; + NSMutableDictionary *queryParameters; + + // This is the first call for this request, so we need to set the connection class + urlString = [NSString stringWithUTF8String:url]; + methodString = [NSString stringWithUTF8String:method]; + + headers = [NSMutableDictionary dictionary]; + queryParameters = [NSMutableDictionary dictionary]; + + // Retrieve the headers and query parameters from the connection + HKConnectionValuesFromBlock( + connection, MHD_HEADER_KIND, + ^(__attribute__((unused)) enum MHD_ValueKind kind, NSString *key, NSString *value) { + [headers setObject:value forKey:key]; + return YES; + }); + HKConnectionValuesFromBlock( + connection, MHD_GET_ARGUMENT_KIND, + ^(__attribute__((unused)) enum MHD_ValueKind kind, NSString *key, NSString *value) { + [queryParameters setObject:value forKey:key]; + return YES; + }); + + request = [[HKHTTPRequest alloc] initWithMethod:methodString + URL:[NSURL URLWithString:urlString] + headers:headers + queryParameters:queryParameters]; + + // Set the request object as the connection class + // This is a __bridge_retained cast, so we need to release the object later on + *con_cls = (__bridge_retained void *) (request); + } else { + // This is a subsequent call for this request, so we need to retrieve the request + // object from the connection class + request = (__bridge HKHTTPRequest *) (*con_cls); + } + + NSLog(@"Request body size: %lu", *upload_data_size); + NSLog(@"Headers: %@", [request headers]); + + // If we have upload data, we need to process it. Otherwise, we can continue + // processing the request. + if (*upload_data_size != 0) { + NSUInteger dataLength; + + NSLog(@"Received %lu bytes of upload data", *upload_data_size); + + dataLength = *upload_data_size; + [request appendBytesToHTTPBody:upload_data length:dataLength]; + + // Tell libmicrohttpd that we processed this portion of data + *upload_data_size = 0; + return MHD_YES; + } else { + return [server _sendResponseForRequest:request + connection:connection + URL:[request URL] + method:[request method]]; + } + } +} + +// A MHD_RequestCompletedCallback to release the request object when the request is completed. +static void requestCompletedCallback(__attribute__((unused)) void *cls, + __attribute__((unused)) struct MHD_Connection *connection, + void **con_cls, + __attribute__((unused)) enum MHD_RequestTerminationCode toe) { + @autoreleasepool { + if (*con_cls != NULL) { + // Cast the result to void to indicate to the compiler that we are intentionally + // ignoring the return value while transferring ownership to ARC. + (void) (__bridge_transfer HKHTTPRequest *) (*con_cls); + *con_cls = NULL; + } + } +} + +@implementation HKHTTPServer { + struct MHD_Daemon *_daemon; +} + ++ (instancetype)serverWithPort:(NSUInteger)port { + return [[self alloc] initWithPort:port]; +} + +- (instancetype)initWithPort:(NSUInteger)port { + self = [super init]; + if (self) { + _port = port; + _router = [HKRouter + routerWithRoutes:@[] + notFoundHandler:^HKHTTPResponse *(__attribute__((unused)) HKHTTPRequest *request) { + return [HKHTTPResponse responseWithStatus:404]; + }]; + } + return self; +} + +- (BOOL)startWithError:(NSError **)error { + _daemon = + MHD_start_daemon(MHD_USE_AUTO_INTERNAL_THREAD, (unsigned short) _port, NULL, NULL, + &accessHandler, (__bridge void *) (self), MHD_OPTION_NOTIFY_COMPLETED, + requestCompletedCallback, NULL, MHD_OPTION_END); + if (!_daemon) { + if (error) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; + } + return NO; + } + return YES; +} + +- (void)stop { + if (_daemon) { + MHD_stop_daemon(_daemon); + _daemon = NULL; + } +} + +/* + Get connection information, and search for a registered handler in the router. + If a handler is found, execute it and return the result, otherwise execute + the notFoundHandler. + + This method is called by the requestHandler MHD_AccessHandlerCallback. +*/ +- (enum MHD_Result)_sendResponseForRequest:(HKHTTPRequest *)request + connection:(struct MHD_Connection *)conn + URL:(NSURL *)URL + method:(NSString *)method { + struct MHD_Response *mhd_response; + int returnCode; + + HKHTTPResponse *response = nil; + HKHandlerBlock middlewareHandler = nil; + NSData *responseData = nil; + NSDictionary *responseHeaders = nil; + + middlewareHandler = [[self router] middleware]; + // Check if a middleware handler is registered + if (middlewareHandler) { + response = middlewareHandler(request); + } + + // If middleware set a response, use it. Otherwise, use the response from the router. + if (response == nil) { + HKHandlerBlock handler = [[self router] handlerForRequest:request]; + // Execute the installed handler block + response = handler(request); + } + + responseData = [response data]; + responseHeaders = [response headers]; + + // If we have response data, create a response from it. Otherwise, create an empty response. + if (responseData) { + // We need to copy the response data, as we do not have direct control over the lifetime + // of the NSData object. + mhd_response = MHD_create_response_from_buffer( + [responseData length], (void *) [responseData bytes], MHD_RESPMEM_MUST_COPY); + if (!mhd_response) { + return MHD_NO; + } + } else { + mhd_response = MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); + } + + if (responseHeaders) { + for (NSString *key in [responseHeaders allKeys]) { + NSString *value = [responseHeaders objectForKey:key]; + MHD_add_response_header(mhd_response, [key UTF8String], [value UTF8String]); + } + } + + returnCode = MHD_queue_response(conn, (unsigned int) [response status], mhd_response); + MHD_destroy_response(mhd_response); + return returnCode; +} + +@end diff --git a/Libraries/MicroHTTPKit/Source/HKRouter.m b/Libraries/MicroHTTPKit/Source/HKRouter.m new file mode 100644 index 0000000..3eb182d --- /dev/null +++ b/Libraries/MicroHTTPKit/Source/HKRouter.m @@ -0,0 +1,80 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +@interface HKRouter () +@property (nonatomic, readwrite) NSArray *routes; +@end + +@implementation HKRoute + ++ (instancetype)routeWithPath:(NSString *)path + method:(NSString *)method + handler:(HKHandlerBlock)handler { + return [[self alloc] initWithPath:path method:method handler:handler]; +} + +- (instancetype)initWithPath:(NSString *)path + method:(NSString *)method + handler:(HKHandlerBlock)handler { + self = [super init]; + + if (self) { + _path = [path copy]; + _handler = [handler copy]; + _method = [method copy]; + } + + return self; +} + +@end + +@implementation HKRouter { + NSMutableArray *_routes; +} + ++ (instancetype)routerWithRoutes:(NSArray *)routes + notFoundHandler:(HKHandlerBlock)notFoundHandler { + return [[self alloc] initWithRoutes:routes notFoundHandler:notFoundHandler]; +} + +- (instancetype)initWithRoutes:(NSArray *)routes + notFoundHandler:(HKHandlerBlock)notFoundHandler { + self = [super init]; + + if (self) { + _routes = [NSMutableArray arrayWithArray:routes]; + _notFoundHandler = [notFoundHandler copy]; + } + + return self; +} + +- (HKHandlerBlock)handlerForRequest:(HKHTTPRequest *)request { + NSString *requestPath; + requestPath = [[request URL] path]; + + for (HKRoute *route in [self routes]) { + if ([requestPath isEqualToString:[route path]] && + [[request method] isEqualToString:[route method]]) { + return [route handler]; + } + } + + return [self notFoundHandler]; +} + +- (void)registerRoute:(HKRoute *)route { + [_routes addObject:route]; +} + +- (NSArray *)routes { + return [_routes copy]; +} + +@end diff --git a/Libraries/MicroHTTPKit/Tests/main.h b/Libraries/MicroHTTPKit/Tests/main.h new file mode 100644 index 0000000..49673d6 --- /dev/null +++ b/Libraries/MicroHTTPKit/Tests/main.h @@ -0,0 +1,10 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import + +// Path to the resource folder +extern NSString *resourcePath; diff --git a/Libraries/MicroHTTPKit/Tests/main.m b/Libraries/MicroHTTPKit/Tests/main.m new file mode 100644 index 0000000..478cf0a --- /dev/null +++ b/Libraries/MicroHTTPKit/Tests/main.m @@ -0,0 +1,25 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#import "main.h" +#import + +NSString *resourcePath = @""; + +// This is the entrypoint for each unit test +int main(int argc, const char *argv[]) { + BOOL res; + @autoreleasepool { + // Set resource path if provided + if (argc == 2) { + resourcePath = [NSString stringWithUTF8String:argv[1]]; + } + + res = [[GSXCTestRunner sharedRunner] runAll]; + } + + return res == YES ? 0 : 1; +} diff --git a/Libraries/MicroHTTPKit/Tests/meson.build b/Libraries/MicroHTTPKit/Tests/meson.build new file mode 100644 index 0000000..a07667c --- /dev/null +++ b/Libraries/MicroHTTPKit/Tests/meson.build @@ -0,0 +1,18 @@ + + +common_objc_args = '-Wno-gnu' +common_dependencies = [xctest_dep] +common_link_with = [microhttpkit_lib] +common_include_dirs = include_directories('../') +common_resource_dir = join_paths(meson.current_source_dir(), 'Resources') + +# Define the first executable +routing = executable( + 'routing', + ['routing.m', 'main.m'], + objc_args: common_objc_args, + dependencies: common_dependencies, + link_with: common_link_with, + include_directories: common_include_dirs +) +test('Routing Test', routing) diff --git a/Libraries/MicroHTTPKit/Tests/routing.m b/Libraries/MicroHTTPKit/Tests/routing.m new file mode 100644 index 0000000..039b47a --- /dev/null +++ b/Libraries/MicroHTTPKit/Tests/routing.m @@ -0,0 +1,246 @@ +/* MicroHTTPKit - A small libmicrohttpd wrapper + * Copyright (C) 2023 Hugo Melder + * + * SPDX-License-Identifier: MIT + */ + +#include "MicroHTTPKit/HKHTTPResponse.h" +#import +#import + +#import "main.h" + +static const NSString *REQUEST_BODY_STRING = @"Hello, World!"; +static const NSString *RESPONSE_STRING = @"Received!"; + +@interface Routing : XCTestCase ++ (NSData *)_sendRequest:(NSURL *)url response:(NSHTTPURLResponse **)resp error:(NSError **)error; +@end + +@implementation Routing + ++ (NSData *)_sendRequest:(NSURL *)url response:(NSHTTPURLResponse **)resp error:(NSError **)error { + NSURLRequest *request; + NSURLResponse *response; + NSData *data; + + request = [NSURLRequest requestWithURL:url]; + + data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error]; + if (!data) { + NSLog(@"Failed to send request to %@ with error %@", url, [*error localizedDescription]); + return nil; + } + + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response; + *resp = httpResponse; + + NSLog(@"HTTP response status code: %ld", (long) [httpResponse statusCode]); + } + + return data; +} + +- (void)testRouteGET { + HKHTTPServer *server; + HKRoute *route; + NSError *error = NULL; + NSURL *url; + NSData *data; + NSHTTPURLResponse *responseObj = nil; + + server = [[HKHTTPServer alloc] initWithPort:8080]; + XCTAssertNotNil(server, @"Server is valid"); + + route = [HKRoute + routeWithPath:@"/test" + method:HKHTTPMethodGET + handler:^(HKHTTPRequest *request) { + XCTAssertNotNil(request, @"Handler called with valid HKHTTPRequest"); + XCTAssertNotNil([request method], @"Method property in request is valid"); + XCTAssertNotNil([request URL], @"URL property in request is valid"); + + XCTAssertEqualObjects([request method], HKHTTPMethodGET, @"Is a GET request"); + + return [HKHTTPResponse + responseWithData:[RESPONSE_STRING dataUsingEncoding:NSUTF8StringEncoding] + status:200]; + }]; + XCTAssertNotNil(route, @"route is valid"); + + [[server router] registerRoute:route]; + + NSArray *routes = [[server router] routes]; + XCTAssertNotNil(routes, @"Routes are valid"); + XCTAssertEqual([routes count], 1, @"Routes count is 1"); + XCTAssertEqualObjects([routes objectAtIndex:0], route, @"Routes are equal"); + + XCTAssertTrue([server startWithError:&error], @"Server started successfully"); + XCTAssert(!error, @"Server started without error"); + + // Simple GET request + url = [NSURL URLWithString:@"http://localhost:8080/test"]; + data = [Routing _sendRequest:url response:&responseObj error:&error]; + XCTAssertNotNil(data, @"Response data is valid"); + XCTAssert(!error, @"Request sent without error"); + XCTAssertNotNil(responseObj, @"Response object is valid"); + XCTAssertEqual([responseObj statusCode], 200, @"HTTP status code is 200"); + + NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(str, RESPONSE_STRING, @"Response data is valid"); + + // Testing the default not-found handler + url = [NSURL URLWithString:@"http://localhost:8080/invalid"]; + data = [Routing _sendRequest:url response:&responseObj error:&error]; + XCTAssertNotNil(data, @"Response data is valid"); + XCTAssert(!error, @"Request sent without error"); + XCTAssertNotNil(responseObj, @"Response object is valid"); + XCTAssertEqual([responseObj statusCode], 404, @"HTTP status code is 404"); + + [server stop]; +} + +- (void)testPOST { + HKHTTPServer *server; + HKRoute *route; + NSError *error = NULL; + NSURL *url; + NSData *data; + NSHTTPURLResponse *responseObj = nil; + + server = [[HKHTTPServer alloc] initWithPort:8080]; + XCTAssertNotNil(server, @"Server is valid"); + + route = [HKRoute + routeWithPath:@"/test" + method:HKHTTPMethodPOST + handler:^(HKHTTPRequest *request) { + XCTAssertNotNil(request, @"Handler called with valid HKHTTPRequest"); + XCTAssertNotNil([request method], @"Method property in request is valid"); + XCTAssertNotNil([request URL], @"URL property in request is valid"); + + XCTAssertEqualObjects([request method], HKHTTPMethodPOST, @"Is a POST request"); + + NSData *data = [request HTTPBody]; + NSString *body = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + + XCTAssertNotNil(data, @"Body data is valid"); + // FIXME: We need to implement a post-processor in HKHTTPServer.m for the body + // data to be correctly passed + + return [HKHTTPResponse + responseWithData:[RESPONSE_STRING dataUsingEncoding:NSUTF8StringEncoding] + status:200]; + }]; + XCTAssertNotNil(route, @"route is valid"); + + [[server router] registerRoute:route]; + + NSArray *routes = [[server router] routes]; + XCTAssertNotNil(routes, @"Routes are valid"); + XCTAssertEqual([routes count], 1, @"Routes count is 1"); + XCTAssertEqualObjects([routes objectAtIndex:0], route, @"Routes are equal"); + + XCTAssertTrue([server startWithError:&error], @"Server started successfully"); + XCTAssert(!error, @"Server started without error"); + + // Simple POST request + url = [NSURL URLWithString:@"http://localhost:8080/test"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setHTTPMethod:@"POST"]; + [request setHTTPBody:[REQUEST_BODY_STRING dataUsingEncoding:NSUTF8StringEncoding]]; + + data = [NSURLConnection sendSynchronousRequest:request + returningResponse:&responseObj + error:&error]; + XCTAssertNotNil(data, @"Response data is valid"); + XCTAssert(!error, @"Request sent without error"); + XCTAssertNotNil(responseObj, @"Response object is valid"); + XCTAssertEqual([responseObj statusCode], 200, @"HTTP status code is 200"); + + NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + XCTAssertNotNil(str, @"String from response data is valid"); + XCTAssertEqualObjects(str, RESPONSE_STRING, @"Response data is valid"); +} + +- (void)testMiddleware { + HKHTTPServer *server; + NSHTTPURLResponse *responseObj; + NSError *error = NULL; + + server = [[HKHTTPServer alloc] initWithPort:8081]; + XCTAssertNotNil(server, @"Server is valid"); + + [[server router] setMiddleware:^HKHTTPResponse *(HKHTTPRequest *request) { + NSDictionary *response; + NSError *error = NULL; + HKHTTPJSONResponse *responseObj; + XCTAssertNotNil(request, @"Request to middleware is valid"); + + response = @{ + @"method" : [request method], + @"url" : [[request URL] absoluteString], + @"headers" : [request headers], + @"queryParameters" : [request queryParameters] + }; + + responseObj = [HKHTTPJSONResponse responseWithJSONObject:response status:200 error:&error]; + XCTAssertNotNil(responseObj, @"Response object is valid"); + XCTAssert(!error, @"Response object created without error"); + + return responseObj; + }]; + + XCTAssertTrue([server startWithError:NULL], @"Server started successfully"); + + // Simple GET request + NSURL *url = [NSURL URLWithString:@"http://localhost:8081/middlewareTest"]; + NSData *data = [Routing _sendRequest:url response:&responseObj error:&error]; + XCTAssertNotNil(data, @"Response data is valid"); + XCTAssert(!error, @"Request sent without error"); + + NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + XCTAssertNotNil(response, @"Response data is valid"); + XCTAssert(!error, @"Response data is valid"); + + XCTAssertEqual([responseObj statusCode], 200, @"HTTP status code is 200"); + XCTAssertEqualObjects([response objectForKey:@"method"], @"GET", @"Method is GET"); + XCTAssertEqualObjects([response objectForKey:@"url"], @"/middlewareTest", + @"URL is /middlewareTest"); + XCTAssertEqualObjects([response objectForKey:@"queryParameters"], @{}, + @"Query parameters are empty"); + + // Simple GET Request with additional headers and query parameters + + NSURLResponse *rawURLResponse; + + url = [NSURL URLWithString:@"http://localhost:8081/middlewareTest?foo=bar"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + data = [NSURLConnection sendSynchronousRequest:request + returningResponse:&rawURLResponse + error:&error]; + XCTAssertNotNil(data, @"Response data is valid"); + XCTAssert(!error, @"Request sent without error"); + + response = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + XCTAssertNotNil(response, @"Response data is valid"); + XCTAssert(!error, @"Response data is valid"); + + XCTAssertEqual([responseObj statusCode], 200, @"HTTP status code is 200"); + XCTAssertEqualObjects([response objectForKey:@"method"], @"GET", @"Method is GET"); + XCTAssertEqualObjects([response objectForKey:@"url"], @"/middlewareTest", + @"URL is /middlewareTest"); + XCTAssertEqualObjects([response objectForKey:@"queryParameters"], @{@"foo" : @"bar"}, + @"Query parameters are foo=bar"); + XCTAssertGreaterThan([[response objectForKey:@"headers"] count], 0, @"Headers are not empty"); + XCTAssertEqualObjects([[response objectForKey:@"headers"] objectForKey:@"Accept"], + @"application/json", @"Accept header is application/json"); + + [server stop]; +} + +@end diff --git a/Libraries/MicroHTTPKit/meson.build b/Libraries/MicroHTTPKit/meson.build new file mode 100644 index 0000000..b3eb090 --- /dev/null +++ b/Libraries/MicroHTTPKit/meson.build @@ -0,0 +1,103 @@ +project('MicroHTTPKit', 'objc', version : '0.2.0', default_options : ['warning_level=3']) + +pkg = import('pkgconfig') + +# Ensure clang is used for Objective-C +objc_compiler = meson.get_compiler('objc') +if objc_compiler.get_id() != 'clang' + error('Clang is required for this project. Please set CC=clang, and OBJC=clang before running Meson.') +endif + +dependencies_to_link = [] +# Common Objective-C flags +objc_flags = [] + +if host_machine.system() != 'darwin' + # Objective-C (GNUstep) support from gnustep-config + gnustep_config = find_program('gnustep-config', required: true) + if not gnustep_config.found() + error('GNUstep is required for this project. Please install GNUstep and ensure gnustep-config is in your PATH. You might want to source GNUstep.sh before running Meson.') + endif + + gnustep_flags = run_command(gnustep_config, '--objc-flags', check: true).stdout().strip().split() + gnustep_base_libs = run_command(gnustep_config, '--base-libs', check: true).stdout().strip().split() + + # Filter out flags that are handled by Meson's built-in options + # or result in warnings (-MMD) + foreach flag : gnustep_flags + if flag != '-Wall' and flag != '-g' and flag != '-O2' and flag != '-MMD' + objc_flags += flag + endif + endforeach + + add_project_link_arguments(gnustep_base_libs, language: 'objc') +else + # Properly link against the Foundation framework + foundation_dep = dependency('appleframeworks', modules: ['Foundation']) + dependencies_to_link += foundation_dep + + add_project_link_arguments('-lobjc', language: 'objc') +endif + +# Enable ARC (Automatic Reference Counting) +objc_flags += '-fobjc-arc' + +# Add Objective-C flags +add_project_arguments(objc_flags, language: 'objc') + +# Add libmicrohttpd dependency +libmicrohttpd_dep = dependency('libmicrohttpd', required: true) +dependencies_to_link += libmicrohttpd_dep + +source = [ + # Objc files + 'Source/HKHTTPServer.m', + 'Source/HKRouter.m', + 'Source/HKHTTPRequest.m', + 'Source/HKHTTPRequest+Private.m', + 'Source/HKHTTPResponse.m', + 'Source/HKHTTPConstants.m', +] + +headers = [ + 'MicroHTTPKit/MicroHTTPKit.h', + 'MicroHTTPKit/HKHTTPServer.h', + 'MicroHTTPKit/HKRouter.h', + 'MicroHTTPKit/HKHTTPRequest.h', + 'MicroHTTPKit/HKHTTPResponse.h', + 'MicroHTTPKit/HKHTTPConstants.h', +] + +include_dirs = include_directories( + 'MicroHTTPKit', +) + +# Build MicroHTTPKit +microhttpkit_lib = library( + 'microhttpkit', + source, + dependencies: dependencies_to_link, + include_directories: include_dirs, + install: true, +) + +install_headers( + headers, + install_dir: join_paths(get_option('prefix'), get_option('includedir'), 'MicroHTTPKit'), +) + +pkg.generate(libraries : microhttpkit_lib, + version : meson.project_version(), + name : 'libmicrohttpkit', + filebase : 'microhttpkit', + description : 'A simple HTTP server written in Objective-C.') + + +# Testing +xctest_dep = dependency('XCTest', required: false) + +if xctest_dep.found() + subdir('Tests') +else + message('XCTest not found. Skipping unit tests.') +endif