ObjWebServer  Artifact [763f8c34cf]

Artifact 763f8c34cf1dbbaf4b58ad24b0a1ebfb1dabefdf4f13723a9e2ee7235df4c66b:


/*
 * ObjWebServer
 * Copyright (C) 2018, 2019  Jonathan Schleifer <js@heap.zone>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#import <ObjFW/ObjFW.h>

#import "Module.h"

#define BUFFER_SIZE 4096

@interface StaticModule: OFPlugin <Module>
{
	OFString *_root;
	OFDictionary OF_GENERIC(OFString *, OFString *) *_MIMETypes;
}
@end

@interface StaticModule_FileSender: OFObject <OFStreamDelegate>
{
@public
	OFFile *_file;
	OFHTTPResponse *_response;
}
@end

static OFData *
readData(OFStream *stream)
{
	void *buffer;
	OFData *ret;

	if ((buffer = malloc(BUFFER_SIZE)) == NULL)
		@throw [OFOutOfMemoryException
		    exceptionWithRequestedSize: BUFFER_SIZE];

	@try {
		size_t length = [stream readIntoBuffer: buffer
						length: BUFFER_SIZE];

		ret = [OFData dataWithItemsNoCopy: buffer
					    count: length
				     freeWhenDone: true];
	} @catch (id e) {
		free(buffer);
		@throw e;
	}

	return ret;
}

@implementation StaticModule_FileSender
- (void)dealloc
{
	[_file release];
	[_response release];

	[super dealloc];
}

- (OFData *)stream: (OF_KINDOF(OFStream *))stream
      didWriteData: (OFData *)data
      bytesWritten: (size_t)bytesWritten
	 exception: (id)exception
{
	if (exception != nil || [_file isAtEndOfStream])
		return nil;

	return readData(_file);
}
@end

@implementation StaticModule
- (void)dealloc
{
	[_root release];

	[super dealloc];
}

- (void)parseConfig: (OFXMLElement *)config
{
	OFMutableDictionary OF_GENERIC(OFString *, OFString *) *MIMETypes;

	_root = [[[config elementForName: @"root"] stringValue] copy];
	if (_root == nil) {
		[of_stderr writeString:
		    @"Error parsing config: No <root/> element!"];
		[OFApplication terminateWithStatus: 1];
	}

	MIMETypes = [OFMutableDictionary dictionary];
	for (OFXMLElement *MIMEType in [config elementsForName: @"mime-type"]) {
		OFString *extension =
		    [[MIMEType attributeForName: @"extension"] stringValue];
		OFString *type =
		    [[MIMEType attributeForName: @"type"] stringValue];

		if (extension == nil) {
			[of_stderr writeString:
			    @"Error parsing config: "
			    @"<mime-type/> has no extension attribute!"];
			[OFApplication terminateWithStatus: 1];
		}
		if (type == nil) {
			[of_stderr writeString:
			    @"Error parsing config: "
			    @"<mime-type/> has no type attribute!"];
			[OFApplication terminateWithStatus: 1];
		}

		[MIMETypes setObject: type
			      forKey: extension];
	}
	[MIMETypes makeImmutable];
	_MIMETypes = [MIMETypes mutableCopy];
}

- (bool)handleRequest: (OFHTTPRequest *)request
	  requestBody: (OFStream *)requestBody
	     response: (OFHTTPResponse *)response
{
	OFURL *URL = [[request URL] URLByStandardizingPath];
	OFString *path = [URL path];
	OFMutableDictionary *headers = [OFMutableDictionary dictionary];
	OFFileManager *fileManager;
	bool firstComponent = true;
	OFString *MIMEType;
	StaticModule_FileSender *fileSender;

	for (OFString *component in [URL pathComponents]) {
		if (firstComponent && [component length] != 0)
			return false;

		if ([component isEqual: @"."] || [component isEqual: @".."])
			return false;

		firstComponent = false;
	}

	/* TODO: Properly handle for OSes that do not use UNIX paths */
	if (![path hasPrefix: @"/"])
		return false;

	path = [_root stringByAppendingString: path];

	fileManager = [OFFileManager defaultManager];
	if ([fileManager directoryExistsAtPath: path])
		path = [path stringByAppendingPathComponent: @"index.html"];

	if (![fileManager fileExistsAtPath: path]) {
		[response setStatusCode: 404];
		return false;
	}

	MIMEType = [_MIMETypes objectForKey: [path pathExtension]];
	if (MIMEType == nil)
		MIMEType = [_MIMETypes objectForKey: @""];

	if (MIMEType != nil)
		[headers setObject: MIMEType
			    forKey: @"Content-Type"];

	fileSender = [[[StaticModule_FileSender alloc] init] autorelease];
	fileSender->_file = [[OFFile alloc] initWithPath: path
						    mode: @"r"];
	fileSender->_response = [response retain];

	[response setStatusCode: 200];
	[response setHeaders: headers];
	[response setDelegate: fileSender];
	[response asyncWriteData: readData(fileSender->_file)];

	return true;
}
@end

StaticModule *
init_plugin(void)
{
	return [[[StaticModule alloc] init] autorelease];
}