/*
* Copyright (c) 2008-2023 Jonathan Schleifer <js@nil.im>
*
* All rights reserved.
*
* This file is part of ObjFW. It may be distributed under the terms of the
* Q Public License 1.0, which can be found in the file LICENSE.QPL included in
* the packaging of this file.
*
* Alternatively, it may be distributed under the terms of the GNU General
* Public License, either version 2 or 3, which can be found in the file
* LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
* file.
*/
#include "config.h"
#include <errno.h>
#import "OFApplication.h"
#import "OFFile.h"
#import "OFFileManager.h"
#import "OFHTTPRequest.h"
#import "OFHTTPResponse.h"
#import "OFHTTPServer.h"
#import "OFIRI.h"
#import "OFLocale.h"
#import "OFNumber.h"
#import "OFOptionsParser.h"
#import "OFStdIOStream.h"
#import "OFGetItemAttributesFailedException.h"
#import "OFInvalidFormatException.h"
#import "OFOpenItemFailedException.h"
@interface OFHTTPD: OFObject <OFApplicationDelegate, OFHTTPServerDelegate>
{
OFHTTPServer *_server;
}
@end
OF_APPLICATION_DELEGATE(OFHTTPD)
static OFString *
safeLocalPathForIRI(OFIRI *IRI)
{
OFString *path = IRI.IRIByStandardizingPath.path;
if (![path hasPrefix: @"/"])
return nil;
path = [path substringWithRange: OFMakeRange(1, path.length - 1)];
#if defined(OF_WINDOWS) || defined(OF_MSDOS)
if ([path containsString: @":"] || [path containsString: @"\\"])
#elif defined(OF_AMIGAOS)
if ([path containsString: @":"] || [path hasPrefix: @"/"])
#else
/* Shouldn't even be possible after standardization, but just in case */
if ([path hasPrefix: @"/"])
#endif
return nil;
/*
* After -[IRIByStandardizingPath], everything representing parent
* directory should be at the beginning, so in theory checking the
* first component should be enough. But it does not hurt being
* paranoid and checking all components, just in case.
*/
for (OFString *component in [path componentsSeparatedByString: @"/"])
if ([component isEqual: @".."])
return nil;
return path;
}
@implementation OFHTTPD
- (void)applicationDidFinishLaunching: (OFNotification *)notification
{
OFString *directory, *host;
unsigned long long port = 0;
const OFOptionsParserOption options[] = {
{ 'd', @"directory", 1, NULL, &directory },
{ 'H', @"host", 1, NULL, &host },
{ 'p', @"port", 1, NULL, NULL },
{ '\0', nil, 0, NULL, NULL }
};
OFFileManager *fileManager = [OFFileManager defaultManager];
OFOptionsParser *optionsParser;
OFUnichar option;
OFMutableIRI *serverIRI;
optionsParser = [OFOptionsParser parserWithOptions: options];
while ((option = [optionsParser nextOption]) != '\0') {
switch (option) {
case 'd':
[fileManager changeCurrentDirectoryPath:
optionsParser.argument];
OFLog(@"Serving directory %@",
fileManager.currentDirectoryPath);
break;
case 'p':
@try {
port = optionsParser.argument.longLongValue;
if (port > UINT16_MAX)
@throw [OFInvalidFormatException
exception];
} @catch (OFInvalidFormatException *e) {
[OFStdErr writeLine: OF_LOCALIZED(
@"invalid_port",
@"%[prog]: Port must be between 0 and "
@"65536!",
@"prog", [OFApplication programName])];
[OFApplication terminateWithStatus: 1];
}
break;
case ':':
if (optionsParser.lastLongOption != nil)
[OFStdErr writeLine: OF_LOCALIZED(
@"long_argument_missing",
@"%[prog]: Argument for option --%[opt] "
@"missing",
@"prog", [OFApplication programName],
@"opt", optionsParser.lastLongOption)];
else {
OFString *optStr = [OFString
stringWithFormat: @"%C",
optionsParser.lastOption];
[OFStdErr writeLine: OF_LOCALIZED(
@"argument_missing",
@"%[prog]: Argument for option -%[opt] "
@"missing",
@"prog", [OFApplication programName],
@"opt", optStr)];
}
[OFApplication terminateWithStatus: 1];
break;
/* case '=': */
case '?':
if (optionsParser.lastLongOption != nil)
[OFStdErr writeLine: OF_LOCALIZED(
@"unknown_long_option",
@"%[prog]: Unknown option: --%[opt]",
@"prog", [OFApplication programName],
@"opt", optionsParser.lastLongOption)];
else {
OFString *optStr = [OFString
stringWithFormat: @"%C",
optionsParser.lastOption];
[OFStdErr writeLine: OF_LOCALIZED(
@"unknown_option",
@"%[prog]: Unknown option: -%[opt]",
@"prog", [OFApplication programName],
@"opt", optStr)];
}
[OFApplication terminateWithStatus: 1];
break;
}
}
if (host == nil)
host = @"127.0.0.1";
_server = [[OFHTTPServer alloc] init];
_server.host = host;
_server.port = (uint16_t)port;
_server.delegate = self;
[_server start];
serverIRI = [OFMutableIRI IRIWithScheme: @"http"];
serverIRI.host = _server.host;
serverIRI.port = [OFNumber numberWithUnsignedShort: _server.port];
OFLog(@"Started server on %@", serverIRI.string);
}
- (void)server: (OFHTTPServer *)server
didReceiveRequest: (OFHTTPRequest *)request
requestBody: (OFStream *)requestBody
response: (OFHTTPResponse *)response
{
OFString *path;
OFLog(@"Handling %s request %@",
OFHTTPRequestMethodName(request.method), request);
if (request.method != OFHTTPRequestMethodGet &&
request.method != OFHTTPRequestMethodHead) {
response.statusCode = 405;
return;
}
path = safeLocalPathForIRI(request.IRI);
if (path == nil) {
response.statusCode = 403;
return;
}
if ([[OFFileManager defaultManager] directoryExistsAtPath: path])
path = [path stringByAppendingPathComponent: @"index.html"];
if (request.method == OFHTTPRequestMethodHead) {
OFFileAttributes attributes;
OFNumber *size;
@try {
attributes = [[OFFileManager defaultManager]
attributesOfItemAtPath: path];
} @catch (OFGetItemAttributesFailedException *e) {
if (e.errNo == EACCES)
response.statusCode = 403;
else
response.statusCode = 404;
return;
}
response.statusCode = 200;
if ((size = [attributes objectForKey: OFFileSize]) != nil)
response.headers = [OFDictionary
dictionaryWithObject: size.description
forKey: @"Content-Length"];
} else {
OFFile *file;
OFLog(@"Sending file %@", path);
@try {
file = [OFFile fileWithPath: path mode: @"r"];
} @catch (OFOpenItemFailedException *e) {
if (e.errNo == EACCES)
response.statusCode = 403;
else
response.statusCode = 404;
return;
}
response.statusCode = 200;
/* TODO: Async stream copy */
while (!file.atEndOfStream) {
char buffer[4096];
size_t length;
length = [file readIntoBuffer: buffer length: 4096];
[response writeBuffer: buffer length: length];
}
}
}
@end