From 58c708af23d6ad855ea0cd1b61c2b87f26fc6988 Mon Sep 17 00:00:00 2001 From: Glenn L McGrath Date: Sun, 5 Jan 2003 04:01:56 +0000 Subject: [PATCH] New applet: httpd, by Glenn Engel --- include/applets.h | 3 + include/usage.h | 15 +- networking/Config.in | 14 + networking/Makefile.in | 1 + networking/httpd.c | 1349 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 networking/httpd.c diff --git a/include/applets.h b/include/applets.h index 5d8e7bb68..177e82352 100644 --- a/include/applets.h +++ b/include/applets.h @@ -236,6 +236,9 @@ #ifdef CONFIG_HOSTNAME APPLET(hostname, hostname_main, _BB_DIR_BIN, _BB_SUID_NEVER) #endif +#ifdef CONFIG_HTTPD + APPLET(httpd, httpd_main, _BB_DIR_USR_SBIN, _BB_SUID_NEVER) +#endif #ifdef CONFIG_HUSH APPLET_NOUSAGE("hush", hush_main, _BB_DIR_BIN, _BB_SUID_NEVER) #endif diff --git a/include/usage.h b/include/usage.h index b5687115d..dfcc89626 100644 --- a/include/usage.h +++ b/include/usage.h @@ -775,7 +775,20 @@ #define hostname_example_usage \ "$ hostname\n" \ "sage \n" - +#ifdef CONFIG_FEATURE_HTTPD_BASIC_AUTH + #define USAGE_HTTPD_BASIC_AUTH(a) a +#else + #define USAGE_HTTPD_BASIC_AUTH(a) +#endif +#define httpd_trivial_usage \ + "[-p ] [-d/-e ]" USAGE_HTTPD_BASIC_AUTH(" [-c ] [-r ]") +#define httpd_full_usage \ + "Listens for incoming http server requests.\n"\ + "Options:\n" \ + "\t-p PORT\tServer port (default 80).\n" \ + USAGE_HTTPD_BASIC_AUTH("\t-c FILE\tSpecifies configuration file. (default httpd.conf)\n\t-r REALM\tAuthentication Realm for Basic Authentication\n") \ + "\t-e STRING\tHtml encode STRING\n" \ + "\t-d STRING\tURL decode STRING\n" #define hwclock_trivial_usage \ "[-r|--show] [-s|--hctosys] [-w|--systohc] [-l|--localtime] [-u|--utc]" #define hwclock_full_usage \ diff --git a/networking/Config.in b/networking/Config.in index b622b65f3..ecd3e570a 100644 --- a/networking/Config.in +++ b/networking/Config.in @@ -29,6 +29,20 @@ config CONFIG_HOSTNAME help Please submit a patch to add help text for this item. +config CONFIG_HTTPD + bool "httpd" + default n + help + Serve web pages via an HTTP server. + +config CONFIG_FEATURE_HTTPD_BASIC_AUTH + bool " Enable Basic Authentication and IP address checking" + default n + depends on CONFIG_HTTPD + help + Utilizes /etc/httpd.conf for security settings allowing + ip address filtering and basic authentication on a per url basis. + config CONFIG_IFCONFIG bool "ifconfig" default n diff --git a/networking/Makefile.in b/networking/Makefile.in index c2ae451a4..7e9a6fdd2 100644 --- a/networking/Makefile.in +++ b/networking/Makefile.in @@ -26,6 +26,7 @@ NETWORKING-y:= NETWORKING-$(CONFIG_FTPGET) += ftpgetput.o NETWORKING-$(CONFIG_FTPPUT) += ftpgetput.o NETWORKING-$(CONFIG_HOSTNAME) += hostname.o +NETWORKING-$(CONFIG_HTTPD) += httpd.o NETWORKING-$(CONFIG_IFCONFIG) += ifconfig.o NETWORKING-$(CONFIG_IFUPDOWN) += ifupdown.o NETWORKING-$(CONFIG_IP) += ip.o diff --git a/networking/httpd.c b/networking/httpd.c new file mode 100644 index 000000000..bceb89ba3 --- /dev/null +++ b/networking/httpd.c @@ -0,0 +1,1349 @@ +/* + * httpd implementation for busybox + * + * Copyright (C) 2002 Glenn Engel + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + ***************************************************************************** + * + * Typical usage: + * cd /var/www + * httpd + * This is equivalent to + * cd /var/www + * httpd -p 80 -c /etc/httpd.conf -r "Web Server Authentication" + * + * When a url contains "cgi-bin" it is assumed to be a cgi script. The + * server changes directory to the location of the script and executes it + * after setting QUERY_STRING and other environment variables. If url args + * are included in the url or as a post, the args are placed into decoded + * environment variables. e.g. /cgi-bin/setup?foo=Hello%20World will set + * the $CGI_foo environment variable to "Hello World". + * + * The server can also be invoked as a url arg decoder and html text encoder + * as follows: + * foo=`httpd -d $foo` # decode "Hello%20World" as "Hello World" + * bar=`httpd -e ""` # encode as "<Hello World>" + * + * httpd.conf has the following format: + +ip:10.10. # Allow any address that begins with 10.10. +ip:172.20. # Allow 172.20.x.x +ip:127.0.0.1 # Allow local loopback connections +/cgi-bin:foo:bar # Require user foo, pwd bar on urls starting with /cgi-bin +/:admin:setup # Require user admin, pwd setup on urls starting with / + + * + * To open up the server: + * ip:* # Allow any IP address + * /:* # no password required for urls starting with / (all) + * + * Processing of the file stops on the first sucessful match. If the file + * is not found, the server is assumed to be wide open. + * + ***************************************************************************** + * + * Desired enhancements: + * cache httpd.conf + * support tinylogin + * + */ +#include +#include /* for isspace */ +#include /* for varargs */ +#include /* for strerror */ +#include /* for malloc */ +#include +#include +#include /* for close */ +#include +#include +#include /* for connect and socket*/ +#include /* for sockaddr_in */ +#include +#include +#include +#include + +static const char httpdVersion[] = "busybox httpd/1.13 3-Jan-2003"; + +// #define DEBUG 1 +#ifndef HTTPD_STANDALONE +#include +#include +// Note: xfuncs are not used because we want the server to keep running +// if something bad happens due to a malformed user request. +// As a result, all memory allocation is checked rigorously +#else +/* standalone */ +#define CONFIG_FEATURE_HTTPD_BASIC_AUTH +void show_usage() +{ + fprintf(stderr,"Usage: httpd [-p ] [-c configFile] [-d/-e ] [-r realm]\n"); +} +#endif + +/* minimal global vars for busybox */ +#ifndef ENVSIZE +#define ENVSIZE 50 +#endif +int debugHttpd; +static char **envp; +static int envCount; +static char *realm = "Web Server Authentication"; +static char *configFile; + +static const char* const suffixTable [] = { + ".htm.html", "text/html", + ".jpg.jpeg", "image/jpeg", + ".gif", "image/gif", + ".png", "image/png", + ".txt.h.c.cc.cpp", "text/plain", + 0,0 + }; + +typedef enum +{ + HTTP_OK = 200, + HTTP_UNAUTHORIZED = 401, /* authentication needed, respond with auth hdr */ + HTTP_NOT_FOUND = 404, + HTTP_INTERNAL_SERVER_ERROR = 500, + HTTP_NOT_IMPLEMENTED = 501, /* used for unrecognized requests */ + HTTP_BAD_REQUEST = 400, /* malformed syntax */ +#if 0 /* future use */ + HTTP_CONTINUE = 100, + HTTP_SWITCHING_PROTOCOLS = 101, + HTTP_CREATED = 201, + HTTP_ACCEPTED = 202, + HTTP_NON_AUTHORITATIVE_INFO = 203, + HTTP_NO_CONTENT = 204, + HTTP_MULTIPLE_CHOICES = 300, + HTTP_MOVED_PERMANENTLY = 301, + HTTP_MOVED_TEMPORARILY = 302, + HTTP_NOT_MODIFIED = 304, + HTTP_PAYMENT_REQUIRED = 402, + HTTP_FORBIDDEN = 403, + HTTP_BAD_GATEWAY = 502, + HTTP_SERVICE_UNAVAILABLE = 503, /* overload, maintenance */ + HTTP_RESPONSE_SETSIZE=0xffffffff +#endif +} HttpResponseNum; + +typedef struct +{ + HttpResponseNum type; + const char *name; + const char *info; +} HttpEnumString; + +static const HttpEnumString httpResponseNames[] = { + { HTTP_OK, "OK" }, + { HTTP_NOT_IMPLEMENTED, "Not Implemented", + "The requested method is not recognized by this server." }, + { HTTP_UNAUTHORIZED, "Unauthorized", "" }, + { HTTP_NOT_FOUND, "Not Found", + "The requested URL was not found on this server." }, + { HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error" + "Internal Server Error" }, + { HTTP_BAD_REQUEST, "Bad Request" , + "Unsupported method.\n" }, +#if 0 + { HTTP_CREATED, "Created" }, + { HTTP_ACCEPTED, "Accepted" }, + { HTTP_NO_CONTENT, "No Content" }, + { HTTP_MULTIPLE_CHOICES, "Multiple Choices" }, + { HTTP_MOVED_PERMANENTLY, "Moved Permanently" }, + { HTTP_MOVED_TEMPORARILY, "Moved Temporarily" }, + { HTTP_NOT_MODIFIED, "Not Modified" }, + { HTTP_FORBIDDEN, "Forbidden", "" }, + { HTTP_BAD_GATEWAY, "Bad Gateway", "" }, + { HTTP_SERVICE_UNAVAILABLE, "Service Unavailable", "" }, +#endif +}; + +/**************************************************************************** + * + > $Function: encodeString() + * + * $Description: Given a string, html encode special characters. + * This is used for the -e command line option to provide an easy way + * for scripts to encode result data without confusing browsers. The + * returned string pointer is memory allocated by malloc(). + * + * $Parameters: + * (const char *) string . . The first string to encode. + * + * $Return: (char *) . . . .. . . A pointer to the encoded string. + * + * $Errors: Returns a null string ("") if memory is not available. + * + ****************************************************************************/ +static char *encodeString(const char *string) +{ + /* take the simple route and encode everything */ + /* could possibly scan once to get length. */ + int len = strlen(string); + char *out = (char*)malloc(len*5 +1); + char *p=out; + char ch; + if (!out) return ""; + while ((ch = *string++)) + { + // very simple check for what to encode + if (isalnum(ch)) *p++ = ch; + else p += sprintf(p,"&#%d", (unsigned char) ch); + } + *p=0; + return out; +} + +/**************************************************************************** + * + > $Function: decodeString() + * + * $Description: Given a URL encoded string, convert it to plain ascii. + * Since decoding always makes strings smaller, the decode is done in-place. + * Thus, callers should strdup() the argument if they do not want the + * argument modified. The return is the original pointer, allowing this + * function to be easily used as arguments to other functions. + * + * $Parameters: + * (char *) string . . . The first string to decode. + * + * $Return: (char *) . . . . A pointer to the decoded string (same as input). + * + * $Errors: None + * + ****************************************************************************/ +static char *decodeString(char *string) +{ + /* note that decoded string is always shorter than original */ + char *orig = string; + char *ptr = string; + while (*ptr) + { + if (*ptr == '+') { *string++ = ' '; ptr++; } + else if (*ptr != '%') *string++ = *ptr++; + else + { + unsigned int value; + sscanf(ptr+1,"%2X",&value); + *string++ = value; + ptr += 3; + } + } + *string = '\0'; + return orig; +} + + +/**************************************************************************** + * + > $Function: addEnv() + * + * $Description: Add an enviornment variable setting to the global list. + * A NAME=VALUE string is allocated, filled, and added to the list of + * environment settings passed to the cgi execution script. + * + * $Parameters: + * (char *) name . . . The environment variable name. + * (char *) value . . The value to which the env variable is set. + * + * $Return: (void) + * + * $Errors: Silently returns if the env runs out of space to hold the new item + * + ****************************************************************************/ +static void addEnv(const char *name, const char *value) +{ + char *s; + if (envCount >= ENVSIZE) return; + if (!value) value = ""; + s=(char*)malloc(strlen(name)+strlen(value)+2); + if (s) + { + sprintf(s,"%s=%s",name, value); + envp[envCount++]=s; + envp[envCount]=0; + } +} + +/**************************************************************************** + * + > $Function: addEnvCgi + * + * $Description: Create environment variables given a URL encoded arg list. + * For each variable setting the URL encoded arg list, create a corresponding + * environment variable. URL encoded arguments have the form + * name1=value1&name2=value2&name3=value3 + * + * $Parameters: + * (char *) pargs . . . . A pointer to the URL encoded arguments. + * + * $Return: None + * + * $Errors: None + * + ****************************************************************************/ +static void addEnvCgi(const char *pargs) +{ + char *args; + if (pargs==0) return; + + /* args are a list of name=value&name2=value2 sequences */ + args = strdup(pargs); + while (args && *args) + { + char *sep; + char *name=args; + char *value=strchr(args,'='); + char *cginame; + if (!value) break; + *value++=0; + sep=strchr(value,'&'); + if (sep) + { + *sep=0; + args=sep+1; + } + else + { + sep = value + strlen(value); + args = 0; /* no more */ + } + cginame=(char*)malloc(strlen(decodeString(name))+5); + if (!cginame) break; + sprintf(cginame,"CGI_%s",name); + addEnv(cginame,decodeString(value)); + free(cginame); + } +} + +#ifdef CONFIG_FEATURE_HTTPD_BASIC_AUTH +static const unsigned char base64ToBin[] = { + 255 /* ' ' */, 255 /* '!' */, 255 /* '"' */, 255 /* '#' */, + 1 /* '$' */, 255 /* '%' */, 255 /* '&' */, 255 /* ''' */, + 255 /* '(' */, 255 /* ')' */, 255 /* '*' */, 62 /* '+' */, + 255 /* ',' */, 255 /* '-' */, 255 /* '.' */, 63 /* '/' */, + 52 /* '0' */, 53 /* '1' */, 54 /* '2' */, 55 /* '3' */, + 56 /* '4' */, 57 /* '5' */, 58 /* '6' */, 59 /* '7' */, + 60 /* '8' */, 61 /* '9' */, 255 /* ':' */, 255 /* ';' */, + 255 /* '<' */, 00 /* '=' */, 255 /* '>' */, 255 /* '?' */, + 255 /* '@' */, 00 /* 'A' */, 01 /* 'B' */, 02 /* 'C' */, + 03 /* 'D' */, 04 /* 'E' */, 05 /* 'F' */, 06 /* 'G' */, + 7 /* 'H' */, 8 /* 'I' */, 9 /* 'J' */, 10 /* 'K' */, + 11 /* 'L' */, 12 /* 'M' */, 13 /* 'N' */, 14 /* 'O' */, + 15 /* 'P' */, 16 /* 'Q' */, 17 /* 'R' */, 18 /* 'S' */, + 19 /* 'T' */, 20 /* 'U' */, 21 /* 'V' */, 22 /* 'W' */, + 23 /* 'X' */, 24 /* 'Y' */, 25 /* 'Z' */, 255 /* '[' */, + 255 /* '\' */, 255 /* ']' */, 255 /* '^' */, 255 /* '_' */, + 255 /* '`' */, 26 /* 'a' */, 27 /* 'b' */, 28 /* 'c' */, + 29 /* 'd' */, 30 /* 'e' */, 31 /* 'f' */, 32 /* 'g' */, + 33 /* 'h' */, 34 /* 'i' */, 35 /* 'j' */, 36 /* 'k' */, + 37 /* 'l' */, 38 /* 'm' */, 39 /* 'n' */, 40 /* 'o' */, + 41 /* 'p' */, 42 /* 'q' */, 43 /* 'r' */, 44 /* 's' */, + 45 /* 't' */, 46 /* 'u' */, 47 /* 'v' */, 48 /* 'w' */, + 49 /* 'x' */, 50 /* 'y' */, 51 /* 'z' */, 255 /* '{' */, + 255 /* '|' */, 255 /* '}' */, 255 /* '~' */, 255 /* '' */ +}; + +/**************************************************************************** + * + > $Function: decodeBase64() + * + > $Description: Decode a base 64 data stream as per rfc1521. + * Note that the rfc states that none base64 chars are to be ignored. + * Since the decode always results in a shorter size than the input, it is + * OK to pass the input arg as an output arg. + * + * $Parameters: + * (void *) outData. . . Where to place the decoded data. + * (size_t) outDataLen . The length of the output data string. + * (void *) inData . . . A pointer to a base64 encoded string. + * (size_t) inDataLen . The length of the input data string. + * + * $Return: (char *) . . . . A pointer to the decoded string (same as input). + * + * $Errors: None + * + ****************************************************************************/ +static size_t decodeBase64(void *outData, size_t outDataLen, + void *inData, size_t inDataLen) +{ + int i = 0; + unsigned char *in = inData; + unsigned char *out = outData; + unsigned long ch = 0; + while (inDataLen && outDataLen) + { + unsigned char conv = 0; + unsigned char newch; + + while (inDataLen) + { + inDataLen--; + newch = *in++; + if ((newch < '0') || (newch > 'z')) continue; + conv = base64ToBin[newch - 32]; + if (conv == 255) continue; + break; + } + ch = (ch << 6) | conv; + i++; + if (i== 4) + { + if (outDataLen >= 3) + { + *(out++) = (unsigned char) (ch >> 16); + *(out++) = (unsigned char) (ch >> 8); + *(out++) = (unsigned char) ch; + outDataLen-=3; + } + + i = 0; + } + + if ((inDataLen == 0) && (i != 0)) + { + /* error - non multiple of 4 chars on input */ + break; + } + + } + + /* return the actual number of chars in output array */ + return out-(unsigned char*) outData; +} +#endif + +/**************************************************************************** + * + > $Function: perror_and_exit() + * + > $Description: A helper function to print an error and exit. + * + * $Parameters: + * (const char *) msg . . . A 'context' message to include. + * + * $Return: None + * + * $Errors: None + * + ****************************************************************************/ +static void perror_exit(const char *msg) +{ + perror(msg); + exit(1); +} + + +/**************************************************************************** + * + > $Function: strncmpi() + * + * $Description: compare two strings without regard to case. + * + * $Parameters: + * (char *) a . . . . . The first string. + * (char *) b . . . . . The second string. + * (int) n . . . . . . The number of chars to compare. + * + * $Return: (int) . . . . . . 0 if strings equal. 1 if a>b, -1 if b < a. + * + * $Errors: None + * + ****************************************************************************/ +#define __toupper(c) ((('a' <= (c))&&((c) <= 'z')) ? ((c) - 'a' + 'A') : (c)) +#define __tolower(c) ((('A' <= (c))&&((c) <= 'Z')) ? ((c) - 'A' + 'a') : (c)) +static int strncmpi(const char *a, const char *b,int n) +{ + char a1,b1; + a1 = b1 = 0; + + while(n-- && ((a1 = *a++) != '\0') && ((b1 = *b++) != '\0')) + { + if(a1 == b1) continue; /* No need to convert */ + a1 = __tolower(a1); + b1 = __tolower(b1); + if(a1 != b1) break; /* No match, abort */ + } + if (n>=0) + { + if(a1 > b1) return 1; + if(a1 < b1) return -1; + } + return 0; +} + +/**************************************************************************** + * + > $Function: openServer() + * + * $Description: create a listen server socket on the designated port. + * + * $Parameters: + * (int) port . . . The port to listen on for connections. + * + * $Return: (int) . . . A connection socket. -1 for errors. + * + * $Errors: None + * + ****************************************************************************/ +static int openServer(int port) +{ + struct sockaddr_in lsocket; + int fd; + + /* create the socket right now */ + /* inet_addr() returns a value that is already in network order */ + memset(&lsocket, 0, sizeof(lsocket)); + lsocket.sin_family = AF_INET; + lsocket.sin_addr.s_addr = INADDR_ANY; + lsocket.sin_port = htons(port) ; + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd >= 0) + { + /* tell the OS it's OK to reuse a previous address even though */ + /* it may still be in a close down state. Allows bind to succeed. */ + int one = 1; +#ifdef SO_REUSEPORT + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (char*)&one, sizeof(one)) ; +#else + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one)) ; +#endif + if (bind(fd, (struct sockaddr *)&lsocket, sizeof(lsocket)) == 0) + { + listen(fd, 9); + signal(SIGCHLD, SIG_IGN); /* prevent zombie (defunct) processes */ + } + else + { + perror("failure to bind to server port"); + shutdown(fd,0); + close(fd); + fd = -1; + } + } + else + { + fprintf(stderr,"httpd: unable to create socket \n"); + } + return fd; +} + +static int sendBuf(int s, char *buf, int len) +{ + if (len == -1) len = strlen(buf); + return send(s, buf, len, 0); +} + +/**************************************************************************** + * + > $Function: sendHeaders() + * + * $Description: Create and send HTTP response headers. + * The arguments are combined and sent as one write operation. Note that + * IE will puke big-time if the headers are not sent in one packet and the + * second packet is delayed for any reason. If contentType is null the + * content type is assumed to be text/html + * + * $Parameters: + * (int) s . . . The http socket. + * (HttpResponseNum) responseNum . . . The result code to send. + * (const char *) contentType . . . . A string indicating the type. + * (int) contentLength . . . . . . . . Content length. -1 if unknown. + * (time_t) expire . . . . . . . . . . Expiration time (secs since 1970) + * + * $Return: (int) . . . . Always 0 + * + * $Errors: None + * + ****************************************************************************/ +static int sendHeaders(int s, HttpResponseNum responseNum , + const char *contentType, + int contentLength, time_t expire) +{ + char buf[1200]; + const char *responseString = ""; + const char *infoString = 0; + unsigned int i; + time_t timer = time(0); + char timeStr[80]; + for (i=0; + i < (sizeof(httpResponseNames)/sizeof(httpResponseNames[0])); i++) + { + if (httpResponseNames[i].type != responseNum) continue; + responseString = httpResponseNames[i].name; + infoString = httpResponseNames[i].info; + break; + } + if (infoString || !contentType) + { + contentType = "text/html"; + } + + sprintf(buf, "HTTP/1.0 %d %s\nContent-type: %s\r\n", + responseNum, responseString, contentType); + + /* emit the current date */ + strftime(timeStr, sizeof(timeStr), + "%a, %d %b %Y %H:%M:%S GMT",gmtime(&timer)); + sprintf(buf+strlen(buf), "Date: %s\r\n", timeStr); + sprintf(buf+strlen(buf), "Connection: close\r\n"); + if (expire) + { + strftime(timeStr, sizeof(timeStr), + "%a, %d %b %Y %H:%M:%S GMT",gmtime(&expire)); + sprintf(buf+strlen(buf), "Expire: %s\r\n", timeStr); + } + + if (responseNum == HTTP_UNAUTHORIZED) + { + sprintf(buf+strlen(buf), + "WWW-Authenticate: Basic realm=\"%s\"\r\n", realm); + } + if (contentLength != -1) + { + int len = strlen(buf); + sprintf(buf+len,"Content-length: %d\r\n", contentLength); + } + strcat(buf,"\r\n"); + if (infoString) + { + sprintf(buf+strlen(buf), + "%d %s\n" + "

%d %s

\n%s\n\n", + responseNum, responseString, + responseNum, responseString, + infoString); + } +#ifdef DEBUG + if (debugHttpd) fprintf(stderr,"Headers:'%s'", buf); +#endif + sendBuf(s, buf,-1); + return 0; +} + +/**************************************************************************** + * + > $Function: getLine() + * + * $Description: Read from the socket until an end of line char found. + * + * Characters are read one at a time until an eol sequence is found. + * + * $Parameters: + * (int) s . . . . . The socket fildes. + * (char *) buf . . Where to place the read result. + * (int) maxBuf . . Maximum number of chars to fit in buf. + * + * $Return: (int) . . . . number of characters read. -1 if error. + * + ****************************************************************************/ +static int getLine(int s, char *buf, int maxBuf) +{ + int count = 0; + while (recv(s, buf+count, 1, 0) == 1) + { + if (buf[count] == '\r') continue; + if (buf[count] == '\n') + { + buf[count] = 0; + return count; + } + count++; + } + if (count) return count; + else return -1; +} + +/**************************************************************************** + * + > $Function: sendCgi() + * + * $Description: Execute a CGI script and send it's stdout back + * + * Environment variables are set up and the script is invoked with pipes + * for stdin/stdout. If a post is being done the script is fed the POST + * data in addition to setting the QUERY_STRING variable (for GETs or POSTs). + * + * $Parameters: + * (int ) s . . . . . . . . The session socket. + * (const char *) url . . . The requested URL (with leading /). + * (const char *urlArgs). . Any URL arguments. + * (const char *body) . . . POST body contents. + * (int bodyLen) . . . . . Length of the post body. + + * + * $Return: (char *) . . . . A pointer to the decoded string (same as input). + * + * $Errors: None + * + ****************************************************************************/ +static int sendCgi(int s, const char *url, + const char *request, const char *urlArgs, + const char *body, int bodyLen) +{ + int fromCgi[2]; /* pipe for reading data from CGI */ + int toCgi[2]; /* pipe for sending data to CGI */ + + char *argp[] = { 0, 0 }; + int pid=0; + int inFd=inFd; + int outFd; + int firstLine=1; + + do + { + if (pipe(fromCgi) != 0) + { + break; + } + if (pipe(toCgi) != 0) + { + break; + } + + pid = fork(); + if (pid < 0) + { + pid = 0; + break;; + } + + if (!pid) + { + /* child process */ + char *script; + char *directory; + inFd=toCgi[0]; + outFd=fromCgi[1]; + + dup2(inFd, 0); // replace stdin with the pipe + dup2(outFd, 1); // replace stdout with the pipe + if (!debugHttpd) dup2(outFd, 2); // replace stderr with the pipe + close(toCgi[0]); + close(toCgi[1]); + close(fromCgi[0]); + close(fromCgi[1]); + +#if 0 + fcntl(0,F_SETFD, 1); + fcntl(1,F_SETFD, 1); + fcntl(2,F_SETFD, 1); +#endif + + script = (char*) malloc(strlen(url)+2); + if (!script) _exit(242); + sprintf(script,".%s",url); + + envCount=0; + addEnv("SCRIPT_NAME",script); + addEnv("REQUEST_METHOD",request); + addEnv("QUERY_STRING",urlArgs); + addEnv("SERVER_SOFTWARE",httpdVersion); + if (strncmpi(request,"POST",4)==0) addEnvCgi(body); + else addEnvCgi(urlArgs); + + /* + * Most HTTP servers chdir to the cgi directory. + */ + while (*url == '/') url++; // skip leading slash(s) + directory = strdup( url ); + if ( directory == (char*) 0 ) + script = (char*) (url); /* ignore errors */ + else + { + script = strrchr( directory, '/' ); + if ( script == (char*) 0 ) + script = directory; + else + { + *script++ = '\0'; + (void) chdir( directory ); /* ignore errors */ + } + } + // now run the program. If it fails, use _exit() so no destructors + // get called and make a mess. + execve(script, argp, envp); + +#ifdef DEBUG + fprintf(stderr, "exec failed\n"); +#endif + close(2); + close(1); + close(0); + _exit(242); + } /* end child */ + + /* parent process */ + inFd=fromCgi[0]; + outFd=toCgi[1]; + close(fromCgi[1]); + close(toCgi[0]); + if (body) write(outFd, body, bodyLen); + close(outFd); + + } while (0); + + if (pid) + { + int status; + pid_t dead_pid; + + while (1) + { + struct timeval timeout; + fd_set readSet; + char buf[160]; + int nfound; + int count; + + FD_ZERO(&readSet); + FD_SET(inFd, &readSet); + + /* Now wait on the set of sockets! */ + timeout.tv_sec = 0; + timeout.tv_usec = 10000; + nfound = select(inFd+1, &readSet, 0, 0, &timeout); + + if (nfound <= 0) + { + dead_pid = waitpid(pid, &status, WNOHANG); + if (dead_pid != 0) + { + close(fromCgi[0]); + close(fromCgi[1]); + close(toCgi[0]); + close(toCgi[1]); +#ifdef DEBUG + if (debugHttpd) + { + if (WIFEXITED(status)) + fprintf(stderr,"piped has exited with status=%d\n", WEXITSTATUS(status)); + if (WIFSIGNALED(status)) + fprintf(stderr,"piped has exited with signal=%d\n", WTERMSIG(status)); + } +#endif + pid = -1; + break; + } + } + else + { + // There is something to read + count = read(inFd,buf,sizeof(buf)-1); + // If a read returns 0 at this point then some type of error has + // occurred. Bail now. + if (count == 0) break; + if (count > 0) + { + if (firstLine) + { + /* check to see if the user script added headers */ + if (strcmp(buf,"HTTP")!= 0) + { + write(s,"HTTP/1.0 200 OK\n", 16); + } + if (strstr(buf,"ontent-") == 0) + { + write(s,"Content-type: text/plain\n\n", 26); + } + + firstLine=0; + } + write(s,buf,count); +#ifdef DEBUG + if (debugHttpd) fprintf(stderr,"cgi read %d bytes\n", count); +#endif + } + } + } + } + return 0; +} + +/**************************************************************************** + * + > $Function: sendFile() + * + * $Description: Send a file response to an HTTP request + * + * $Parameters: + * (int) s . . . . . . . The http session socket. + * (const char *) url . . The URL requested. + * + * $Return: (int) . . . . . . Always 0. + * + ****************************************************************************/ +static int sendFile(int s, const char *url) +{ + char *suffix = strrchr(url,'.'); + const char *content = "application/octet-stream"; + int f; + + if (suffix) + { + const char ** table; + for (table = (const char **) &suffixTable[0]; + *table && (strstr(*table, suffix) == 0); table+=2); + if (table) content = *(table+1); + } + + if (*url == '/') url++; + suffix = strchr(url,'?'); + if (suffix) *suffix = 0; + +#ifdef DEBUG + fprintf(stderr,"Sending file '%s'\n", url); +#endif + + f = open(url,O_RDONLY, 0444); + if (f >= 0) + { + char buf[1450]; + int count; + sendHeaders(s, HTTP_OK, content, -1, 0 ); + while ((count = read(f, buf, sizeof(buf)))) + { + sendBuf(s, buf, count); + } + close(f); + } + else + { +#ifdef DEBUG + fprintf(stderr,"Unable to open '%s'\n", url); +#endif + sendHeaders(s, HTTP_NOT_FOUND, "text/html", -1, 0); + } + + return 0; +} + +/**************************************************************************** + * + > $Function: checkPerm() + * + * $Description: Check the permission file for access. + * + * Both IP addresses as well as url pathnames can be specified. If an IP + * address check is desired, the 'path' should be specified as "ip" and the + * dotted decimal IP address placed in request. + * + * For url pathnames, place the url (with leading /) in 'path' and any + * authentication information in request. e.g. "user:pass" + * + ******* + * + * Keep the algorithm simple. + * If config file isn't present, everything is allowed. + * Run down /etc/httpd.hosts a line at a time. + * Stop if match is found. + * Entries are of the form: + * ip:10.10 # any address that begins with 10.10 + * dir:user:pass # dir security for dirs that start with 'dir' + * + * httpd.conf has the following format: + * ip:10.10. # Allow any address that begins with 10.10. + * ip:172.20. # Allow 172.20.x.x + * /cgi-bin:foo:bar # Require user foo, pwd bar on urls starting with /cgi-bin + * /:foo:bar # Require user foo, pwd bar on urls starting with / + * + * To open up the server: + * ip:* # Allow any IP address + * /:* # no password required for urls starting with / (all) + * + * $Parameters: + * (const char *) path . . . . The file path or "ip" for ip addresses. + * (const char *) request . . . User information to validate. + * + * $Return: (int) . . . . . . . . . 1 if request OK, 0 otherwise. + * + ****************************************************************************/ +static int checkPerm(const char *path, const char *request) +{ + FILE *f=NULL; + int rval; + char buf[80]; + char *p; + int ipaddr=0; + + /* If httpd.conf not there assume anyone can get in */ + if (configFile) f = fopen(configFile,"r"); + if(f == NULL) f = fopen("/etc/httpd.conf","r"); + if(f == NULL) f = fopen("httpd.conf","r"); + if(f == NULL) { + return(1); + } + if (strcmp("ip",path) == 0) ipaddr=1; + + rval=0; + + /* This could stand some work */ + while ( fgets(buf, 80, f) != NULL) + { + if(buf[0] == '#') continue; + if(buf[0] == '\0') continue; + for(p = buf + (strlen(buf) - 1); p >= buf; p--) + { + if(isspace(*p)) *p = 0; + } + + p = strchr(buf,':'); + if (!p) continue; + *p++=0; +#ifdef DEBUG + fprintf(stderr,"checkPerm: '%s' ? '%s'\n",buf,path); +#endif + if((ipaddr ? strcmp(buf,path) : strncmp(buf, path, strlen(buf))) == 0) + { + /* match found. Check request */ + if ((strcmp("*",p) == 0) || + (strcmp(p, request) == 0) || + (ipaddr && (strncmp(p, request, strlen(p)) == 0))) + { + rval = 1; + break; + } + + /* reject on first failure for non ipaddresses */ + if (!ipaddr) break; + } + }; + fclose(f); + return(rval); +}; + + +/**************************************************************************** + * + > $Function: handleIncoming() + * + * $Description: Handle an incoming http request. + * + * $Parameters: + * (s) s . . . . . The http request socket. + * + * $Return: (int) . . . Always 0. + * + ****************************************************************************/ +static int handleIncoming(int s) +{ + char buf[8192]; + char url[8192]; /* hold args too initially */ + char credentials[80]; + char request[20]; + long length=0; + int major; + int minor; + char *urlArgs; + char *body=0; + + credentials[0] = 0; + do + { + int count = getLine(s, buf, sizeof(buf)); + int blank; + if (count <= 0) break; + count = sscanf(buf, "%9s %1000s HTTP/%d.%d", request, + url, &major, &minor); + + if (count < 2) + { + /* Garbled request/URL */ +#if 0 + genHttpHeader(&requestInfo, + HTTP_BAD_REQUEST, requestInfo.dataType, + HTTP_LENGTH_UNKNOWN); +#endif + break; + } + + /* If no version info, assume 0.9 */ + if (count != 4) + { + major = 0; + minor = 9; + } + + /* extract url args if present */ + urlArgs = strchr(url,'?'); + if (urlArgs) + { + *urlArgs=0; + urlArgs++; + } + +#ifdef DEBUG + if (debugHttpd) fprintf(stderr,"url='%s', args=%s\n", url, urlArgs); +#endif + + // read until blank line(s) + blank = 0; + while ((count = getLine(s, buf, sizeof(buf))) >= 0) + { + if (count == 0) + { + if (major > 0) break; + blank++; + if (blank == 2) break; + } +#ifdef DEBUG + if (debugHttpd) fprintf(stderr,"Header: '%s'\n", buf); +#endif + + /* try and do our best to parse more lines */ + if ((strncmpi(buf, "Content-length:", 15) == 0)) + { + sscanf(buf, "%*s %ld", &length); + } +#ifdef CONFIG_FEATURE_HTTPD_BASIC_AUTH + else if (strncmpi(buf, "Authorization:", 14) == 0) + { + /* We only allow Basic credentials. + * It shows up as "Authorization: Basic " where + * the userid:password is base64 encoded. + */ + char *ptr = buf+14; + while (*ptr == ' ') ptr++; + if (strncmpi(ptr, "Basic", 5) != 0) break; + ptr += 5; + while (*ptr == ' ') ptr++; + memset(credentials, 0, sizeof(credentials)); + decodeBase64(credentials, + sizeof(credentials)-1, + ptr, + strlen(ptr) ); + + } + } + if (!checkPerm(url, credentials)) + { + sendHeaders(s, HTTP_UNAUTHORIZED, 0, -1, 0); + length=-1; + break; /* no more processing */ + } +#else + } +#endif /* CONFIG_FEATURE_HTTPD_BASIC_AUTH */ + + /* we are done if an error occurred */ + if (length == -1) break; + + if (strcmp(url,"/") == 0) strcpy(url,"/index.html"); + + if (length>0) + { + body=(char*) malloc(length+1); + if (body) + { + length = read(s,body,length); + body[length]=0; // always null terminate for safety + urlArgs=body; + } + } + + if (strstr(url,"..") || strstr(url, "httpd.conf")) + { + /* protect from .. path creep */ + sendHeaders(s, HTTP_NOT_FOUND, "text/html", -1, 0); + } + else if (strstr(url,"cgi-bin")) + { + sendCgi(s, url, request, urlArgs, body, length); + } + else if (strncmpi(request,"GET",3) == 0) + { + sendFile(s, url); + } + else + { + sendHeaders(s, HTTP_NOT_IMPLEMENTED, 0, -1, 0); + } + } while (0); + +#ifdef DEBUG + if (debugHttpd) fprintf(stderr,"closing socket\n"); +#endif + if (body) free(body); + shutdown(s,SHUT_WR); + shutdown(s,SHUT_RD); + close(s); + + return 0; +} + +/**************************************************************************** + * + > $Function: miniHttpd() + * + * $Description: The main http server function. + * + * Given an open socket fildes, listen for new connections and farm out + * the processing as a forked process. + * + * $Parameters: + * (int) server. . . The server socket fildes. + * + * $Return: (int) . . . . Always 0. + * + ****************************************************************************/ +static int miniHttpd(int server) +{ + fd_set readfd, portfd; + int nfound; + + FD_ZERO(&portfd); + FD_SET(server, &portfd); + + /* copy the ports we are watching to the readfd set */ + while (1) + { + readfd = portfd ; + + /* Now wait INDEFINATELY on the set of sockets! */ + nfound = select(server+1, &readfd, 0, 0, 0); + + switch (nfound) + { + case 0: + /* select timeout error! */ + break ; + case -1: + /* select error */ + break; + default: + if (FD_ISSET(server, &readfd)) + { + char on; + struct sockaddr_in fromAddr; + char rmt_ip[20]; + int addr; + socklen_t fromAddrLen = sizeof(fromAddr); + int s = accept(server, + (struct sockaddr *)&fromAddr, &fromAddrLen) ; + if (s < 0) + { + continue; + } + addr = ntohl(fromAddr.sin_addr.s_addr); + sprintf(rmt_ip,"%u.%u.%u.%u", + (unsigned char)(addr >> 24), + (unsigned char)(addr >> 16), + (unsigned char)(addr >> 8), + (unsigned char)(addr >> 0)); +#ifdef DEBUG + if (debugHttpd) + { + fprintf(stderr,"httpMini.cpp: connection from IP=%s, port %d\n", + rmt_ip, ntohs(fromAddr.sin_port)); + } +#endif + if(checkPerm("ip", rmt_ip) == 0) + { + close(s); + continue; + } + + /* set the KEEPALIVE option to cull dead connections */ + on = 1; + setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char *) &on, + sizeof (on)); + + if (fork() == 0) + { + /* This is the spawned thread */ + handleIncoming(s); + exit(0); + } + close(s); + } + } + } // while (1) + return 0; +} + +int httpd_main(int argc, char *argv[]) +{ + int server; + int port = 80; + int c; + + /* check if user supplied a port number */ + for (;;) { + c = getopt( argc, argv, "p:ve:d:" +#ifdef CONFIG_FEATURE_HTTPD_BASIC_AUTH + "r:c:" +#endif + ); + if (c == EOF) break; + switch (c) { + case 'v': + debugHttpd=1; + break; + case 'p': + port = atoi(optarg); + break; + case 'd': + printf("%s",decodeString(optarg)); + return 0; + case 'e': + printf("%s",encodeString(optarg)); + return 0; + case 'r': + realm = optarg; + break; + case 'c': + configFile = optarg; + break; + default: + fprintf(stderr,"%s\n", httpdVersion); + show_usage(); + exit(1); + } + } + + envp = (char**) malloc((ENVSIZE+1)*sizeof(char*)); + if (envp == 0) perror_exit("envp alloc"); + + server = openServer(port); + if (server < 0) exit(1); + + if (!debugHttpd) + { + /* remember our current pwd, daemonize, chdir back */ + char *dir = (char *) malloc(256); + if (dir == 0) perror_exit("out of memory for getpwd"); + if (getcwd(dir, 256) == 0) perror_exit("getcwd failed"); + if (daemon(0, 1) < 0) perror_exit("daemon"); + chdir(dir); + free(dir); + } + + miniHttpd(server); + + return 0; +} + +#ifdef HTTPD_STANDALONE +int main(int argc, char *argv[]) +{ + return httpd_main(argc, argv); +} + +#endif