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