blob: e5c4cff8cd98f079d9f5cc6de3ed32136c042c65 [file] [log] [blame]
// webserve.m -- a very simple web server using fork() to handle requests
/* compile with
cc -g -Wall -o webserve webserve.c
*/
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if !defined(_WIN32)
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SOCK_CONST_DATA const void*
#else
#include <stdio.h>
#define WIN32_LEAN_AND_MEAN
#define SOCK_CONST_DATA const char*
#include <windows.h>
#include <winsock2.h>
#include <Iprtrmib.h>
#include <Iphlpapi.h>
#include <io.h>
#define sleep Sleep
static DWORD getpid() {
return GetCurrentProcessId();
}
char *strsep(char **stringp, const char *delim);
#endif
#include <CoreFoundation/CoreFoundation.h>
#define PORT_NUMBER 8080 // set to 80 to listen on the HTTP port
#define LINEBUFFER_SIZE 8192
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
struct clientConn
{
CFWriteStreamRef writeStream;
CFSocketNativeHandle listenSocket;
};
// HTTP request handling
// these are some of the common HTTP response codes
#define HTTP_OK 200
#define HTTP_NOT_FOUND 404
#define HTTP_ERROR 500
// return a string to the browser
#define returnString(httpResult, string, channel) \
returnBuffer((httpResult), (string), (strlen(string)), (channel))
// return a character buffer (not necessarily zero-terminated) to the
// browser (runs in the child)
void returnBuffer (int httpResult, const char *content,
int contentLength, CFWriteStreamRef commChannel)
{
static const CFStringRef headerFmt = CFSTR("HTTP/1.0 %d blah\r\n");
static const CFStringRef contentType = CFSTR("Content-Type: text/html\r\n");
static const CFStringRef contentLenFmt = CFSTR("Content-Length: %d\r\n");
static const CFStringRef endHeader = CFSTR("\r\n");
CFStringRef header = 0, contentLengthStr = 0;
header = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, headerFmt, httpResult);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(header,CFStringGetFastestEncoding(header)),CFStringGetLength(header));
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(contentType,CFStringGetFastestEncoding(contentType)),CFStringGetLength(contentType));
contentLengthStr = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, contentLenFmt, contentLength);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(contentLengthStr,CFStringGetFastestEncoding(contentLengthStr)),CFStringGetLength(contentLengthStr));
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(endHeader,CFStringGetFastestEncoding(endHeader)),CFStringGetLength(endHeader));
CFWriteStreamWrite(commChannel, (const UInt8*)content, contentLength);
} // returnBuffer
// stream back to the browser numbers being counted, with a pause
// between them. The user should see the numbers appear every couple
// of seconds (runs in the child)
void returnNumbers (int number, CFWriteStreamRef commChannel)
{
static const CFStringRef headerFmt = CFSTR("HTTP/1.0 %d blah\r\n");
static const CFStringRef contentType = CFSTR("Content-Type: text/html\r\n");
static const CFStringRef endHeader = CFSTR("\r\n");
static const CFStringRef dataHeaderFmt = CFSTR("<html><head><title>Numbers</title></head><body><h2>The numbers from %d to %d</h2>\n");
static const CFStringRef numberFmt = CFSTR("%d\n");
static const CFStringRef done = CFSTR("<hr>Done\n</body></html>\r\n");
int min = MIN (number, 1);
int max = MAX (number, 1);
CFStringRef header = 0, dataHeader = 0;
header = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, headerFmt, HTTP_OK);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(header,CFStringGetFastestEncoding(header)),CFStringGetLength(header));
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(contentType,CFStringGetFastestEncoding(contentType)),CFStringGetLength(contentType));
// no content length, dynamic
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(endHeader,CFStringGetFastestEncoding(endHeader)),CFStringGetLength(endHeader));
dataHeader = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, dataHeaderFmt, min, max);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(dataHeader,CFStringGetFastestEncoding(dataHeader)),CFStringGetLength(dataHeader));
for (int i = min; i <= max; i++) {
sleep (2);
CFStringRef numberStr = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, numberFmt, i);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(numberStr,CFStringGetFastestEncoding(numberStr)),CFStringGetLength(numberStr));
}
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(done,CFStringGetFastestEncoding(done)),CFStringGetLength(done));
} // returnNumbers
// return a file from the file system, relative to where the webserve
// is running. Note that this doesn't look for any nasty characters
// like '..', so this function is a pretty big security hole
// (runs in the child)
void returnFile (const char *filename, CFWriteStreamRef commChannel)
{
static const CFStringRef headerFmt = CFSTR("HTTP/1.0 %d blah\r\n");
char lineBuffer[LINEBUFFER_SIZE];
const char *mimetype = NULL;
// try to guess the mime type. IE assumes all non-graphic files
// are HTML
if (strstr(filename, ".m") != NULL) {
mimetype = "text/plain";
} else if (strstr(filename, ".h") != NULL) {
mimetype = "text/plain";
} else if (strstr(filename, ".txt") != NULL) {
mimetype = "text/plain";
} else if (strstr(filename, ".tgz") != NULL) {
mimetype = "application/x-compressed";
} else if (strstr(filename, ".html") != NULL) {
mimetype = "text/html";
} else if (strstr(filename, ".htm") != NULL) {
mimetype = "text/html";
} else if (strstr(filename, ".mp3") != NULL) {
mimetype = "audio/mpeg";
}
FILE *file;
file = fopen (filename, "r");
if (file == NULL) {
returnString (HTTP_NOT_FOUND,
"could not find your file. Sorry\n.",
commChannel);
} else {
CFStringRef header = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, headerFmt, HTTP_OK);
CFWriteStreamWrite(commChannel,(const UInt8*)CFStringGetCStringPtr(header,CFStringGetFastestEncoding(header)),CFStringGetLength(header));
if (mimetype != NULL) {
sprintf (lineBuffer, "Content-Type: %s\r\n", mimetype);
CFWriteStreamWrite(commChannel, (const UInt8*)lineBuffer, strlen(lineBuffer));
}
CFWriteStreamWrite(commChannel, (const UInt8*)"\r\n", 2); // no content length, dynamic
#define BUFFER_SIZE (8 * 1024)
char *buffer[BUFFER_SIZE];
int result;
while ((result = fread (buffer, 1, BUFFER_SIZE, file)) > 0) {
CFWriteStreamWrite (commChannel, (const UInt8*)buffer, result);
}
#undef BUFFER_SIZE
}
}
// using the method and the request (the path part of the url),
// generate the data for the user and send it back. (runs in the
// child)
void handleRequest (const char *method,
const char *originalRequest, CFWriteStreamRef commChannel)
{
char* request = strdup (originalRequest);
// we'll use strsep to split this
if (strcmp(method, "GET") != 0) {
returnString (HTTP_ERROR,
"only GETs are supported", commChannel);
goto bailout;
}
char *chunk, *nextString;
nextString = request;
chunk = strsep (&nextString, "/");
// urls start with slashes, so chunk is ""
chunk = strsep (&nextString, "/"); // the leading part of the url
if (strcmp(chunk, "numbers") == 0) {
int number;
// url of the form /numbers/5 to print numbers from 1 to 5
chunk = strsep (&nextString, "/");
number = atoi(chunk);
returnNumbers (number, commChannel);
} else if (strcmp(chunk, "file") == 0) {
chunk = strsep (&nextString, ""); // get the rest of the string
returnFile (chunk, commChannel);
} else {
returnString (HTTP_NOT_FOUND,
"could not handle your request. Sorry\n.",
commChannel);
}
bailout:
fprintf (stderr, "child %ld handled request '%s'\n",
(long)getpid(), originalRequest);
free (request);
}
// read the request from the browser, pull apart the elements of the
// request, and then dispatch it. (runs in the child)
void dispatchRequest (CFReadStreamRef readStream, CFStreamEventType eventType, void* clientCallBackInfo)
{
char lineBuffer[LINEBUFFER_SIZE];
struct clientConn* pClientConn = ((struct clientConn*)clientCallBackInfo);
CFWriteStreamRef writeStream = pClientConn->writeStream;
CFSocketNativeHandle listenSocket = pClientConn->listenSocket;
CFIndex bytesRead = CFReadStreamRead(readStream, (UInt8*)lineBuffer, LINEBUFFER_SIZE);
// this is pretty lame in that it only reads the first line and
// assumes that's the request, subsequently ignoring any headers
// that might be sent.
if (bytesRead > 0) {
// ok, now figure out what they wanted
char *requestElements[3], *nextString, *chunk;
int i = 0;
nextString = lineBuffer;
while (i < 3 && (chunk = strsep (&nextString, " "))) {
requestElements[i] = chunk;
i++;
}
if (i != 3) {
returnString (HTTP_ERROR, "malformed request", writeStream);
goto bailout;
}
handleRequest (requestElements[0], requestElements[1], writeStream);
} else
fprintf (stderr, "read an empty request. exiting\n");
bailout:
fflush (stderr);
if (writeStream) {
CFWriteStreamClose(writeStream);
//CFRelease(writeStream);
}
if (readStream) {
CFReadStreamUnscheduleFromRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
CFReadStreamClose(readStream);
CFRelease(readStream);
}
close(listenSocket);
free(pClientConn);
} // dispatchRequest
// sit blocking on accept until either it breaks out with a signal
// (like SIGCHLD) or a new connection comes in. If it's a new
// connection, fork off a child to process the request
static void acceptRequest(CFSocketRef socket, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
struct sockaddr_in clientAddr;
int port = 0;
const char* addr = 0;
Boolean didSet = 0;
CFSocketNativeHandle listenSocket = *(CFSocketNativeHandle *)data;
if (kCFSocketAcceptCallBack != type) {
fprintf (stderr, "accept called with incorrect type (%d). error: %d/%s\n",
type, errno, strerror(errno));
return;
}
memcpy (&clientAddr, (const void*)address, sizeof(clientAddr));
port = clientAddr.sin_port;
addr = inet_ntoa(clientAddr.sin_addr);
fprintf (stderr, "Accepted connection from %s:%d\n", addr, port);
// child sends output to stderr, so make sure it's drained before moving on
fflush (stderr);
CFReadStreamRef readStream = 0;
CFWriteStreamRef writeStream = 0;
CFStreamCreatePairWithSocket(kCFAllocatorDefault, listenSocket, &readStream, &writeStream);
if (!readStream || !writeStream) {
fprintf (stderr, "Failed to build I/O streams. Error: %d/%s\n",
errno, strerror(errno));
close(listenSocket);
return;
}
CFStreamClientContext clientContext;
memset(&clientContext, 0x00, sizeof(clientContext));
clientContext.info = malloc(sizeof(clientConn));
((struct clientConn*)clientContext.info)->writeStream = writeStream;
((struct clientConn*)clientContext.info)->listenSocket = listenSocket;
didSet = CFReadStreamSetClient(readStream, kCFStreamEventOpenCompleted|kCFStreamEventHasBytesAvailable|kCFStreamEventEndEncountered, (CFReadStreamClientCallBack)&dispatchRequest, &clientContext);
if (didSet)
CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
CFReadStreamOpen(readStream);
CFWriteStreamOpen(writeStream);
}
// this is 100% stolen from chatterserver.m
// start listening on our server port (runs in parent)
CFSocketRef startListening ()
{
int result = 0;
int yes = 1;
CFDataRef address4 = 0;
CFSocketContext socketContext = {0, 0, NULL, NULL, NULL};
CFSocketRef listenOn = CFSocketCreate (kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, (CFSocketCallBack)&acceptRequest, &socketContext);
if (!listenOn) {
fprintf (stderr, "could not make a socket. error: %d / %s\n",
errno, strerror(errno));
return 0;
}
result = setsockopt (CFSocketGetNative (listenOn), SOL_SOCKET, SO_REUSEADDR, (SOCK_CONST_DATA)&yes, sizeof(yes));
if (result == -1) {
fprintf (stderr,
"could not setsockopt to reuse address. %d / %s\n",
errno, strerror(errno));
if (listenOn) CFRelease(listenOn);
listenOn = 0;
return 0;
}
// bind to an address and port
struct sockaddr_in address;
#if !defined(__WIN32__)
address.sin_len = sizeof (struct sockaddr_in);
#endif
address.sin_family = AF_INET;
address.sin_port = htons (PORT_NUMBER);
address.sin_addr.s_addr = htonl (INADDR_ANY);
memset (address.sin_zero, 0, sizeof(address.sin_zero));
address4 = CFDataCreateWithBytesNoCopy (kCFAllocatorDefault, (const UInt8*)&address, sizeof(address), kCFAllocatorDefault);
if (kCFSocketSuccess != CFSocketSetAddress (listenOn, address4)) {
fprintf (stderr, "could not bind socket. error: %d / %s\n",
errno, strerror(errno));
if (listenOn) CFRelease(listenOn);
listenOn = 0;
return 0;
}
return listenOn;
}
int main (int argc, char *argv[])
{
CFSocketRef listenSocket = 0;
CFRunLoopSourceRef source = 0;
listenSocket = startListening ();
if (!listenSocket)
return -1;
source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, listenSocket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
CFRelease(source);
CFRunLoopRun();
return (EXIT_SUCCESS);
}
#if defined(__WIN32__)
/*
* From the BSD sources. Why isn't this in MSVCRTL?
*/
char *strsep(char **stringp, const char *delim) {
char *origin, *p;
const char *pp;
if (stringp || !*stringp || !delim || !*delim) {
return NULL;
}
for(origin = p = *stringp; *p; p++) {
for(pp = delim; *pp; pp++) {
if (*pp == *p) {
*p = 0;
*stringp = *++p ? p : NULL;
}
}
}
return origin;
}
#endif