mirror of https://github.com/Wilfred/difftastic/
Add support for Objective-C
Closes #600 Co-authored-by: Nick Moore <nick@pilotmoon.com>pull/619/head
parent
6338c3b314
commit
db86b28a28
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// HttpServer.h after file
|
||||||
|
//
|
||||||
|
// Created by Nicholas Moore on 20/09/2022.
|
||||||
|
// A line added to the header.
|
||||||
|
//
|
||||||
|
// Added to difftastic test suite by the author.
|
||||||
|
// This source file is released by the author into the public domain.
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef NSDictionary *_Nullable (^PopHttpRequestHandler)(NSURL *url,
|
||||||
|
NSString *method,
|
||||||
|
NSDictionary *headers,
|
||||||
|
NSData *body);
|
||||||
|
|
||||||
|
@interface PopHttpServer : NSObjectqq
|
||||||
|
@property(readonly) uint16_t port; // comment
|
||||||
|
@property(readonly) NSString *host;
|
||||||
|
@property(readonly) NSString *lastError;
|
||||||
|
@property(readonly, getter=isListening) BOOL listening;
|
||||||
|
- (id)initWithPort:(uint16_t)port;
|
||||||
|
- (BOOL)start;
|
||||||
|
- (void)stop;
|
||||||
|
- (void)registerHandler:(NSString *_Nonnull)pathPrefix
|
||||||
|
block:(PopHttpRequestHandler)myblock
|
||||||
|
added:(BOOL *_Nullable)added;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// HttpServer.h
|
||||||
|
//
|
||||||
|
// Created by Nicholas Moore on 20/09/2022.
|
||||||
|
//
|
||||||
|
// Added to difftastic test suite by the author.
|
||||||
|
// This source file is released by the author into the public domain.
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
typedef NSDictionary *_Nullable(^PopHttpRequestHandler)(NSURL *url, NSString *method, NSDictionary *headers, NSData *body);
|
||||||
|
|
||||||
|
@interface PopHttpServer : NSObject
|
||||||
|
@property (readonly) uint16_t port;
|
||||||
|
@property (readonly) NSString *lastError;
|
||||||
|
@property (readonly, getter=isListening) BOOL listening;
|
||||||
|
- (id)initWithPort:(uint16_t)port;
|
||||||
|
- (BOOL)start;
|
||||||
|
- (void)stop;
|
||||||
|
- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock;
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
@ -0,0 +1,346 @@
|
|||||||
|
//
|
||||||
|
// HttpServer.m
|
||||||
|
// Simple HTTP server.
|
||||||
|
// by Nicholas Moore 2023
|
||||||
|
// inspired by http://www.cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html
|
||||||
|
//
|
||||||
|
// Added to difftastic test suite by the author.
|
||||||
|
// This source file is released by the author into the public domain.
|
||||||
|
|
||||||
|
|
||||||
|
#import "PopHttpServer.h"
|
||||||
|
#import "NMKit.h"
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
|
||||||
|
@interface PopHttpServer ()
|
||||||
|
@property (readonly) CFSocketRef socket;
|
||||||
|
@property (readonly) NSFileHandle *listenHandle;
|
||||||
|
@property (readonly) NSMapTable<NSFileHandle *, NSObject *> *httpMessages;
|
||||||
|
@property (readonly) NSMutableDictionary<NSString *, PopHttpRequestHandler> *handlers;
|
||||||
|
@property NSString *lastError;
|
||||||
|
@end
|
||||||
|
|
||||||
|
const NSArray *expressions=@[@NO, @7, @(YES), @3.15, @(9), @-11, @"Goodbye"];
|
||||||
|
|
||||||
|
@implementation PopHttpServer
|
||||||
|
|
||||||
|
- (id)initWithPort:(uint16_t)port
|
||||||
|
{
|
||||||
|
self=[super init];
|
||||||
|
if (self) {
|
||||||
|
self->_port=port;
|
||||||
|
self->_httpMessages=[NSMapTable weakToStrongObjectsMapTable];
|
||||||
|
self->_handlers=[NSMutableDictionary dictionary];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warning: handlers will be called on a background thread!!
|
||||||
|
- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock
|
||||||
|
{
|
||||||
|
[self.handlers setObject:myblock forKey:pathPrefix];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)newHttpMessageForHandle:(NSFileHandle *const)connectionHandle
|
||||||
|
{
|
||||||
|
[self.httpMessages setObject:CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE))
|
||||||
|
forKey:connectionHandle];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)start
|
||||||
|
{
|
||||||
|
NMLogFine(@"Attempting to start HTTP server on port %@", @(self.port));
|
||||||
|
|
||||||
|
// create socket
|
||||||
|
self->_socket = CFSocketCreate(kCFAllocatorDefault,
|
||||||
|
PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 6, NULL
|
||||||
|
);
|
||||||
|
if (!self.socket)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to create socket";
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set reuse flag
|
||||||
|
// "This lets us reclaim the port if it is open but idle (a common occurrence if we restart the program immediately after a crash or killing the application)."
|
||||||
|
const int reuse = true;
|
||||||
|
const int fileDescriptor = CFSocketGetNative(self.socket);
|
||||||
|
if (setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int)) != 0)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to set socket options.";
|
||||||
|
[self stop];
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create address and port
|
||||||
|
struct sockaddr_in address;
|
||||||
|
memset(&address, 0, sizeof(address));
|
||||||
|
address.sin_len = sizeof(address);
|
||||||
|
address.sin_family = AF_INET;
|
||||||
|
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
address.sin_port = htons(self.port);
|
||||||
|
NSData *const addressData = [NSData dataWithBytes:&address length:sizeof(address)];
|
||||||
|
if (CFSocketSetAddress(self.socket, (__bridge CFDataRef)addressData) != kCFSocketSuccess)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to bind socket.";
|
||||||
|
[self stop]; return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add listener for connections
|
||||||
|
self->_listenHandle = [[NSFileHandle alloc]
|
||||||
|
initWithFileDescriptor:fileDescriptor
|
||||||
|
closeOnDealloc:YES];
|
||||||
|
|
||||||
|
// register for notifications
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(receiveIncomingConnectionNotification:)
|
||||||
|
name:NSFileHandleConnectionAcceptedNotification
|
||||||
|
object:self.listenHandle];
|
||||||
|
[self.listenHandle acceptConnectionInBackgroundAndNotify];
|
||||||
|
|
||||||
|
NMLogFine(@"HTTP server started on port %@", @(self.port));
|
||||||
|
return YES;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo everything that was done in start
|
||||||
|
- (void)stop
|
||||||
|
{
|
||||||
|
// close down all open connections
|
||||||
|
for (NSFileHandle *connectionHandle in [self.httpMessages keyEnumerator]) {
|
||||||
|
NMLogFine(@"Closing connection %@", connectionHandle);
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle closeFile];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.listenHandle)
|
||||||
|
{
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleConnectionAcceptedNotification
|
||||||
|
object:self.listenHandle];
|
||||||
|
[self.listenHandle closeFile];
|
||||||
|
self->_listenHandle=nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.socket)
|
||||||
|
{
|
||||||
|
CFSocketInvalidate(self.socket);
|
||||||
|
CFRelease(self.socket);
|
||||||
|
self->_socket=nil;
|
||||||
|
}
|
||||||
|
NMLogFine(@"HTTP server on port %@ stopped", @(self.port));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isListening
|
||||||
|
{
|
||||||
|
return self.socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// notification sent by the listenHandle
|
||||||
|
- (void)receiveIncomingConnectionNotification:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSFileHandle *const connectionHandle=[[notification userInfo] objectForKey:NSFileHandleNotificationFileHandleItem];
|
||||||
|
if(connectionHandle)
|
||||||
|
{
|
||||||
|
[self newHttpMessageForHandle:connectionHandle];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(receiveIncomingDataNotification:)
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle waitForDataInBackgroundAndNotify];
|
||||||
|
}
|
||||||
|
NMLogFine(@"Incoming connection %@", connectionHandle);
|
||||||
|
|
||||||
|
// accept another connection
|
||||||
|
[self.listenHandle acceptConnectionInBackgroundAndNotify];
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSUInteger, DataOutcome) {
|
||||||
|
DataOutcomeContinue,
|
||||||
|
DataOutcomeClose,
|
||||||
|
DataOutcomeKeepAlive,
|
||||||
|
DataOutcomeUnknown,
|
||||||
|
};
|
||||||
|
- (void)receiveIncomingDataNotification:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSFileHandle *const connectionHandle=[notification object];
|
||||||
|
|
||||||
|
// get our stored partial http message for this connection
|
||||||
|
const CFHTTPMessageRef httpMessage=(__bridge CFHTTPMessageRef)[self.httpMessages objectForKey:connectionHandle];
|
||||||
|
|
||||||
|
// perform remaining processing in backgroumd thread
|
||||||
|
NMRunAsyncInBackground(^{
|
||||||
|
const DataOutcome outcome=[self processHttpDataForMessage:httpMessage
|
||||||
|
connection:connectionHandle];
|
||||||
|
NMRunAsyncOnMainThread(^{
|
||||||
|
switch (outcome) {
|
||||||
|
case DataOutcomeClose:
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle closeFile];
|
||||||
|
NMLogFine(@"Closed connection %@; there are %@", connectionHandle, @(self.httpMessages.count));
|
||||||
|
break;
|
||||||
|
case DataOutcomeKeepAlive:
|
||||||
|
[self newHttpMessageForHandle:connectionHandle];
|
||||||
|
NMLogFine(@"Ready for new message on connection %@; there are %@", connectionHandle, @(self.httpMessages.count));
|
||||||
|
// here we deliberately fall through to the Continue case
|
||||||
|
case DataOutcomeContinue:
|
||||||
|
default:
|
||||||
|
NMLogFine(@"Awaiting more data on connection %@", connectionHandle);
|
||||||
|
[connectionHandle waitForDataInBackgroundAndNotify];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DataOutcome)processHttpDataForMessage:(CFHTTPMessageRef)httpMessage connection:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
// get the data (if any)
|
||||||
|
NSData *data=nil;
|
||||||
|
@try {
|
||||||
|
data=[connectionHandle availableData];
|
||||||
|
}
|
||||||
|
@catch(NSException *e) {
|
||||||
|
// Ignore the exception, it normally just means the client
|
||||||
|
// closed the connection from the other end.
|
||||||
|
}
|
||||||
|
NMLogFine(@"Data: %@ bytes received on connection %@", @(data.length), connectionHandle);
|
||||||
|
|
||||||
|
// close if EOF or error parsing into http message
|
||||||
|
if (!data.length || httpMessage ||
|
||||||
|
!CFHTTPMessageAppendBytes(httpMessage, data.bytes, data.length)) {
|
||||||
|
return DataOutcomeClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue if we don't have the full header yet
|
||||||
|
if (!CFHTTPMessageIsHeaderComplete(httpMessage)) {
|
||||||
|
return DataOutcomeContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the Content-Length value
|
||||||
|
NSInteger contentLength=0;
|
||||||
|
CFStringRef contentLengthStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Content-Length"));
|
||||||
|
if (contentLengthStr) {
|
||||||
|
contentLength = CFStringGetIntValue(contentLengthStr);
|
||||||
|
CFRelease(contentLengthStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the Connection header
|
||||||
|
BOOL closeWhenDone=NO;
|
||||||
|
CFStringRef connectionStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Connection"));
|
||||||
|
if (connectionStr) {
|
||||||
|
closeWhenDone = (CFStringCompare(connectionStr, CFSTR("close"), kCFCompareCaseInsensitive)==kCFCompareEqualTo);
|
||||||
|
CFRelease(connectionStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get received data length
|
||||||
|
NSInteger receivedLength;
|
||||||
|
CFDataRef bodyData = CFHTTPMessageCopyBody(httpMessage);
|
||||||
|
if (bodyData) {
|
||||||
|
receivedLength = CFDataGetLength(bodyData);
|
||||||
|
CFRelease(bodyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if full body is recieved
|
||||||
|
if (receivedLength>=contentLength) {
|
||||||
|
NMLogFine(@"Entire message received, responding on connection %@", connectionHandle);
|
||||||
|
[self respondToHttpMessage:httpMessage withConnection:connectionHandle];
|
||||||
|
return closeWhenDone?DataOutcomeClose:DataOutcomeKeepAlive;
|
||||||
|
} else {
|
||||||
|
return DataOutcomeContinue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)respondToHttpMessage:(CFHTTPMessageRef)httpMessage withConnection:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
#ifdef DEBUG
|
||||||
|
NSMutableArray *const logMessage=[NSMutableArray array];
|
||||||
|
void(^log)(NSString *, ...) = ^(NSString *format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
[logMessage addObject:[[NSString alloc] initWithFormat:format arguments:args]];
|
||||||
|
va_end(args);
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// get request info
|
||||||
|
NSURL *const url=CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpMessage));
|
||||||
|
NSString *const method=CFBridgingRelease(CFHTTPMessageCopyRequestMethod(httpMessage));
|
||||||
|
NSDictionary *const headers=CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage));
|
||||||
|
NSData *const body=CFBridgingRelease(CFHTTPMessageCopyBody(httpMessage));
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"%@ value is %@", method, url.absoluteURL);
|
||||||
|
log(@"Body: %@", body);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// my very basic router
|
||||||
|
NSString *const firstPathPart=[[[url.path stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]] componentsSeparatedByString:@"/"] safeFirstObject];
|
||||||
|
NSDictionary *res=nil;
|
||||||
|
PopHttpRequestHandler handler=[self.handlers objectForKey:firstPathPart];
|
||||||
|
if (handler) {
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"Route: %@", firstPathPart);
|
||||||
|
#endif
|
||||||
|
res=handler(url, method, headers, body);
|
||||||
|
}
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"Response: %@", res);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
NMLogFine(@"%@", [logMessage componentsJoinedByString:@"\n"]);
|
||||||
|
// note: prevously was wrapping this in WarpToMain, but seems unnecessary
|
||||||
|
if (res) {
|
||||||
|
[self respondWithBody:res[@"body"] status:res[@"status"] contentType:res[@"contentType"] headers:res[@"headers"] handle:connectionHandle];
|
||||||
|
} else {
|
||||||
|
// redo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)respondWithBody:(NSObject *)bodyObj status:(NSNumber *)status contentType:(NSString *)contentType headers:(NSDictionary<NSString *, NSString *> *)headers handle:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
NSData *bodyData=nil;
|
||||||
|
if ([bodyObj isKindOfClass:[NSString class]]) {
|
||||||
|
bodyData=[(NSString *)bodyObj dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
contentType=[contentType stringByAppendingString:@"; charset=utf-8"];
|
||||||
|
}
|
||||||
|
else if ([bodyObj isKindOfClass:[NSData class]]) {
|
||||||
|
bodyData=(NSData *)bodyObj;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NMLogError(@"Bad bodyObj: %@", bodyObj);
|
||||||
|
bodyData=[NSData data];
|
||||||
|
}
|
||||||
|
|
||||||
|
// create response message
|
||||||
|
CFHTTPMessageRef response=CFHTTPMessageCreateResponse(kCFAllocatorDefault, [status integerValue], NULL, kCFHTTPVersion1_1);
|
||||||
|
[headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull val, BOOL * _Nonnull stop) {
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, (__bridge CFStringRef)key, (__bridge CFStringRef)val);
|
||||||
|
}];
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, CFSTR("content-type"), (__bridge CFStringRef)contentType);
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, CFSTR("content-length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%@", @(bodyData.length)]);
|
||||||
|
CFHTTPMessageSetBody(response, (__bridge CFDataRef)bodyData);
|
||||||
|
CFDataRef messageData = CFHTTPMessageCopySerializedMessage(response);
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
[connectionHandle writeData:(__bridge NSData *)messageData];
|
||||||
|
}
|
||||||
|
@catch (NSException *exception)
|
||||||
|
{
|
||||||
|
// Ignore the exception, it normally just means the client
|
||||||
|
// closed the connection from the other end.
|
||||||
|
}
|
||||||
|
@finally
|
||||||
|
{
|
||||||
|
CFRelease(messageData);
|
||||||
|
CFRelease(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@ -0,0 +1,341 @@
|
|||||||
|
//
|
||||||
|
// HttpServer.m
|
||||||
|
// Simple HTTP server.
|
||||||
|
// by Nicholas Moore 2023
|
||||||
|
// inspired by http://www.cocoawithlove.com/2009/07/simple-extensible-http-server-in-cocoa.html
|
||||||
|
//
|
||||||
|
// Added to difftastic test suite by the author.
|
||||||
|
// This source file is released by the author into the public domain.
|
||||||
|
|
||||||
|
|
||||||
|
#import "PopHttpServer.h"
|
||||||
|
#import "NMKit.h"
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
|
||||||
|
@interface PopHttpServer ()
|
||||||
|
@property (readonly) CFSocketRef socket;
|
||||||
|
@property (readonly) NSFileHandle *listenHandle;
|
||||||
|
@property (readonly) NSMapTable<NSFileHandle *, id> *httpMessages;
|
||||||
|
@property (readonly) NSMutableDictionary<NSString *, PopHttpRequestHandler> *handlers;
|
||||||
|
@property NSString *lastError;
|
||||||
|
@end
|
||||||
|
|
||||||
|
const NSArray *expressions=@[@YES, @6, @(NO), @3.14, @(-9), @-10, @"Hello"];
|
||||||
|
|
||||||
|
@implementation PopHttpServer
|
||||||
|
|
||||||
|
- (id)initWithPort:(uint16_t)port
|
||||||
|
{
|
||||||
|
self=[super init];
|
||||||
|
if (self) {
|
||||||
|
self->_port=port;
|
||||||
|
self->_httpMessages=[NSMapTable weakToStrongObjectsMapTable];
|
||||||
|
self->_handlers=[NSMutableDictionary dictionary];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warning: handlers will be called on a background thread
|
||||||
|
- (void)registerHandler:(NSString *)pathPrefix block:(PopHttpRequestHandler)myblock
|
||||||
|
{
|
||||||
|
[self.handlers setObject:myblock forKey:pathPrefix];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)newHttpMessageForHandle:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
[self.httpMessages setObject:CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, TRUE)) forKey:connectionHandle];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)start
|
||||||
|
{
|
||||||
|
NMLogFine(@"Attempting to start HTTP server on port %@", @(self.port));
|
||||||
|
|
||||||
|
// create socket
|
||||||
|
self->_socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL);
|
||||||
|
if (!self.socket)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to create socket";
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set reuse flag
|
||||||
|
// "This lets us reclaim the port if it is open but idle (a common occurrence if we restart the program immediately after a crash or killing the application)."
|
||||||
|
const int reuse = true;
|
||||||
|
const int fileDescriptor = CFSocketGetNative(self.socket);
|
||||||
|
if (setsockopt(fileDescriptor, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int)) != 0)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to set socket options.";
|
||||||
|
[self stop];
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create address and port
|
||||||
|
struct sockaddr_in address;
|
||||||
|
memset(&address, 0, sizeof(address));
|
||||||
|
address.sin_len = sizeof(address);
|
||||||
|
address.sin_family = AF_INET;
|
||||||
|
address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
address.sin_port = htons(self.port);
|
||||||
|
NSData *const addressData = [NSData dataWithBytes:&address length:sizeof(address)];
|
||||||
|
if (CFSocketSetAddress(self.socket, (__bridge CFDataRef)addressData) != kCFSocketSuccess)
|
||||||
|
{
|
||||||
|
self.lastError=@"Unable to bind socket to address.";
|
||||||
|
[self stop];
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add listener for connections
|
||||||
|
self->_listenHandle = [[NSFileHandle alloc]
|
||||||
|
initWithFileDescriptor:fileDescriptor
|
||||||
|
closeOnDealloc:YES];
|
||||||
|
|
||||||
|
// register for notifications
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(receiveIncomingConnectionNotification:)
|
||||||
|
name:NSFileHandleConnectionAcceptedNotification
|
||||||
|
object:self.listenHandle];
|
||||||
|
[self.listenHandle acceptConnectionInBackgroundAndNotify];
|
||||||
|
|
||||||
|
NMLogFine(@"HTTP server started on port %@", @(self.port));
|
||||||
|
return YES;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo everything in start
|
||||||
|
- (void)stop
|
||||||
|
{
|
||||||
|
// close down all open connections
|
||||||
|
for (NSFileHandle *connectionHandle in [self.httpMessages keyEnumerator]) {
|
||||||
|
NMLogFine(@"Closing connection %@", connectionHandle);
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle closeFile];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.listenHandle)
|
||||||
|
{
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleConnectionAcceptedNotification
|
||||||
|
object:self.listenHandle];
|
||||||
|
[self.listenHandle closeFile];
|
||||||
|
self->_listenHandle=nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.socket)
|
||||||
|
{
|
||||||
|
CFSocketInvalidate(self.socket);
|
||||||
|
CFRelease(self.socket);
|
||||||
|
self->_socket=nil;
|
||||||
|
}
|
||||||
|
NMLogFine(@"HTTP server on port %@ stopped", @(self.port));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isListening
|
||||||
|
{
|
||||||
|
return self.socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
// notification sent by the listenHandle
|
||||||
|
- (void)receiveIncomingConnectionNotification:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSFileHandle *const connectionHandle=[[notification userInfo] objectForKey:NSFileHandleNotificationFileHandleItem];
|
||||||
|
if(connectionHandle)
|
||||||
|
{
|
||||||
|
[self newHttpMessageForHandle:connectionHandle];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(receiveIncomingDataNotification:)
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle waitForDataInBackgroundAndNotify];
|
||||||
|
}
|
||||||
|
NMLogFine(@"Incoming connection %@", connectionHandle);
|
||||||
|
// accept another connection
|
||||||
|
[self.listenHandle acceptConnectionInBackgroundAndNotify];
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSUInteger, DataOutcome) {
|
||||||
|
DataOutcomeContinue,
|
||||||
|
DataOutcomeClose,
|
||||||
|
DataOutcomeKeepAlive,
|
||||||
|
};
|
||||||
|
- (void)receiveIncomingDataNotification:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSFileHandle *const connectionHandle=[notification object];
|
||||||
|
|
||||||
|
// get our stored partial http message for this connection
|
||||||
|
const CFHTTPMessageRef httpMessage=(__bridge CFHTTPMessageRef)[self.httpMessages objectForKey:connectionHandle];
|
||||||
|
|
||||||
|
// perform remaining processing in backgroumd thread
|
||||||
|
NMRunAsyncInBackground(^{
|
||||||
|
const DataOutcome outcome=[self processHttpDataForMessage:httpMessage connection:connectionHandle];
|
||||||
|
NMRunAsyncOnMainThread(^{
|
||||||
|
switch (outcome) {
|
||||||
|
case DataOutcomeClose:
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self
|
||||||
|
name:NSFileHandleDataAvailableNotification
|
||||||
|
object:connectionHandle];
|
||||||
|
[connectionHandle closeFile];
|
||||||
|
NMLogFine(@"Closed connection %@; there are %@", connectionHandle, @(self.httpMessages.count));
|
||||||
|
break;
|
||||||
|
case DataOutcomeKeepAlive:
|
||||||
|
[self newHttpMessageForHandle:connectionHandle];
|
||||||
|
NMLogFine(@"Ready for new message on connection %@; there are %@", connectionHandle, @(self.httpMessages.count));
|
||||||
|
// here we deliberately fall through to the Continue case
|
||||||
|
case DataOutcomeContinue:
|
||||||
|
default:
|
||||||
|
NMLogFine(@"Awaiting more data on connection %@", connectionHandle);
|
||||||
|
[connectionHandle waitForDataInBackgroundAndNotify];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (DataOutcome)processHttpDataForMessage:(CFHTTPMessageRef)httpMessage connection:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
// get the data (if any)
|
||||||
|
NSData *data=nil;
|
||||||
|
@try {
|
||||||
|
data=[connectionHandle availableData];
|
||||||
|
}
|
||||||
|
@catch(NSException *e) {
|
||||||
|
// Ignore the exception, it normally just means the client
|
||||||
|
// closed the connection from the other end.
|
||||||
|
}
|
||||||
|
NMLogFine(@"Data: %@ bytes received on connection %@", @(data.length), connectionHandle);
|
||||||
|
|
||||||
|
// close if EOF or error parsing into http message
|
||||||
|
if (!data.length ||
|
||||||
|
!httpMessage ||
|
||||||
|
!CFHTTPMessageAppendBytes(httpMessage, data.bytes, data.length)) {
|
||||||
|
return DataOutcomeClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue if we don't have the full header yet
|
||||||
|
if (!CFHTTPMessageIsHeaderComplete(httpMessage)) {
|
||||||
|
return DataOutcomeContinue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the Content-Length value
|
||||||
|
NSInteger contentLength=0;
|
||||||
|
CFStringRef contentLengthStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Content-Length"));
|
||||||
|
if (contentLengthStr) {
|
||||||
|
contentLength = CFStringGetIntValue(contentLengthStr);
|
||||||
|
CFRelease(contentLengthStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the Connection header
|
||||||
|
BOOL closeWhenDone=NO;
|
||||||
|
CFStringRef connectionStr = CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Connection"));
|
||||||
|
if (connectionStr) {
|
||||||
|
closeWhenDone = (CFStringCompare(connectionStr, CFSTR("close"), kCFCompareCaseInsensitive)==kCFCompareEqualTo);
|
||||||
|
CFRelease(connectionStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get received data length
|
||||||
|
NSInteger receivedLength=0;
|
||||||
|
CFDataRef bodyData = CFHTTPMessageCopyBody(httpMessage);
|
||||||
|
if (bodyData) {
|
||||||
|
receivedLength = CFDataGetLength(bodyData);
|
||||||
|
CFRelease(bodyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if full body is recieved
|
||||||
|
if (receivedLength>=contentLength) {
|
||||||
|
NMLogFine(@"Entire message received, responding on connection %@", connectionHandle);
|
||||||
|
[self respondToHttpMessage:httpMessage withConnection:connectionHandle];
|
||||||
|
return closeWhenDone?DataOutcomeClose:DataOutcomeKeepAlive;
|
||||||
|
} else {
|
||||||
|
return DataOutcomeContinue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)respondToHttpMessage:(CFHTTPMessageRef)httpMessage withConnection:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
#ifdef DEBUG
|
||||||
|
NSMutableArray *const logMessage=[NSMutableArray array];
|
||||||
|
void(^log)(NSString *, ...) = ^(NSString *format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
[logMessage addObject:[[NSString alloc] initWithFormat:format arguments:args]];
|
||||||
|
va_end(args);
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// get request info
|
||||||
|
NSURL *const url=CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpMessage));
|
||||||
|
NSString *const method=CFBridgingRelease(CFHTTPMessageCopyRequestMethod(httpMessage));
|
||||||
|
NSDictionary *const headers=CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage));
|
||||||
|
NSData *const body=CFBridgingRelease(CFHTTPMessageCopyBody(httpMessage));
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"%@ %@", method, url.absoluteURL);
|
||||||
|
log(@"Body: %@", body);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// my very basic router
|
||||||
|
NSString *const firstPathPart=[[[url.path stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]] componentsSeparatedByString:@"/"] safeFirstObject];
|
||||||
|
NSDictionary *res=nil;
|
||||||
|
PopHttpRequestHandler handler=[self.handlers objectForKey:firstPathPart];
|
||||||
|
if (handler) {
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"Route: %@", firstPathPart);
|
||||||
|
#endif
|
||||||
|
res=handler(url, method, headers, body);
|
||||||
|
}
|
||||||
|
#ifdef DEBUG
|
||||||
|
log(@"Response: %@", res);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
NMLogFine(@"%@", [logMessage componentsJoinedByString:@"\n"]);
|
||||||
|
// note: prevously was wrapping this in WarpToMain, but seems unnecessary
|
||||||
|
if (res) {
|
||||||
|
[self respondWithBody:res[@"body"] status:res[@"status"] contentType:res[@"contentType"] headers:res[@"headers"] handle:connectionHandle];
|
||||||
|
} else {
|
||||||
|
[self respondWithBody:@"Not found\n" status:@(404) contentType:@"text/plain" headers:@{} handle:connectionHandle];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)respondWithBody:(NSObject *)bodyObj status:(NSNumber *)status contentType:(NSString *)contentType headers:(NSDictionary<NSString *, NSString *> *)headers handle:(NSFileHandle *)connectionHandle
|
||||||
|
{
|
||||||
|
NSData *bodyData=nil;
|
||||||
|
if ([bodyObj isKindOfClass:[NSString class]]) {
|
||||||
|
bodyData=[(NSString *)bodyObj dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
contentType=[contentType stringByAppendingString:@"; charset=utf-8"];
|
||||||
|
}
|
||||||
|
else if ([bodyObj isKindOfClass:[NSData class]]) {
|
||||||
|
bodyData=(NSData *)bodyObj;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NMLogError(@"Bad bodyObj: %@", bodyObj);
|
||||||
|
bodyData=[NSData data];
|
||||||
|
}
|
||||||
|
|
||||||
|
// create response message
|
||||||
|
CFHTTPMessageRef response=CFHTTPMessageCreateResponse(kCFAllocatorDefault, [status integerValue], NULL, kCFHTTPVersion1_1);
|
||||||
|
[headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull val, BOOL * _Nonnull stop) {
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, (__bridge CFStringRef)key, (__bridge CFStringRef)val);
|
||||||
|
}];
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Content-Type"), (__bridge CFStringRef)contentType);
|
||||||
|
CFHTTPMessageSetHeaderFieldValue(response, CFSTR("Content-Length"), (__bridge CFStringRef)[NSString stringWithFormat:@"%@", @(bodyData.length)]);
|
||||||
|
CFHTTPMessageSetBody(response, (__bridge CFDataRef)bodyData);
|
||||||
|
CFDataRef messageData = CFHTTPMessageCopySerializedMessage(response);
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
[connectionHandle writeData:(__bridge NSData *)messageData];
|
||||||
|
}
|
||||||
|
@catch (NSException *exception)
|
||||||
|
{
|
||||||
|
// Ignore the exception, it normally just means the client
|
||||||
|
// closed the connection from the other end.
|
||||||
|
}
|
||||||
|
@finally
|
||||||
|
{
|
||||||
|
CFRelease(messageData);
|
||||||
|
CFRelease(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@ -0,0 +1 @@
|
|||||||
|
../tree-sitter-objc/queries/highlights.scm
|
||||||
@ -0,0 +1 @@
|
|||||||
|
tree-sitter-objc/src
|
||||||
Loading…
Reference in New Issue