httpd: add optional support for error pages
authorDenis Vlasenko <vda.linux@googlemail.com>
Tue, 21 Aug 2007 10:26:55 +0000 (10:26 -0000)
committerDenis Vlasenko <vda.linux@googlemail.com>
Tue, 21 Aug 2007 10:26:55 +0000 (10:26 -0000)
(by Pierre Metras <genepi@sympatico.ca>)

networking/Config.in
networking/httpd.c

index 3013be6..5275adc 100644 (file)
@@ -170,6 +170,19 @@ config FEATURE_HTTPD_ENCODE_URL_STR
          For example, httpd -e "<Hello World>" as
          "&#60Hello&#32World&#62".
 
+config FEATURE_HTTPD_ERROR_PAGES
+       bool "Enable support for custom error pages"
+       default n
+       depends on HTTPD
+       help
+         This option allows you to define custom error pages in
+         the configuration file instead of the default HTTP status
+         error pages. For instance, if you add the line:
+               E404:/path/e404.html
+         in the config file, the server will respond the specified
+         '/path/e404.html' file instead of the terse '404 NOT FOUND'
+         message.
+
 config IFCONFIG
        bool "ifconfig"
        default n
index 070e2a9..7e60fc2 100644 (file)
@@ -42,6 +42,7 @@
  * A:10.0.0.0/255.255.255.128  # Allow any address that previous set
  * A:127.0.0.1       # Allow local loopback connections
  * D:*               # Deny from other IP connections
+ * E404:/path/e404.html # /path/e404.html is the 404 (not found) error page
  * /cgi-bin:foo:bar  # Require user foo, pwd bar on urls starting with /cgi-bin/
  * /adm:admin:setup  # Require user admin, pwd setup on urls starting with /adm/
  * /adm:toor:PaSsWd  # or user toor, pwd PaSsWd on urls starting with /adm/
  * subdir http request, any merge is discarded when the process exits.  As a
  * result, the subdir settings only have a lifetime of a single request.
  *
+ * Custom error pages can contain an absolute path or be relative to
+ * 'home_httpd'. Error pages are to be static files (no CGI or script). Error
+ * page can only be defined in the root configuration file and are not taken
+ * into account in local (directories) config files.
  *
  * If -c is not set, an attempt will be made to open the default
  * root configuration file.  If -c is set and the file is not found, the
@@ -131,6 +136,84 @@ typedef struct Htaccess_IP {
        int allow_deny;
 } Htaccess_IP;
 
+enum {
+       HTTP_OK = 200,
+       HTTP_MOVED_TEMPORARILY = 302,
+       HTTP_BAD_REQUEST = 400,       /* malformed syntax */
+       HTTP_UNAUTHORIZED = 401, /* authentication needed, respond with auth hdr */
+       HTTP_NOT_FOUND = 404,
+       HTTP_FORBIDDEN = 403,
+       HTTP_REQUEST_TIMEOUT = 408,
+       HTTP_NOT_IMPLEMENTED = 501,   /* used for unrecognized requests */
+       HTTP_INTERNAL_SERVER_ERROR = 500,
+       HTTP_CONTINUE = 100,
+#if 0   /* future use */
+       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_NOT_MODIFIED = 304,
+       HTTP_PAYMENT_REQUIRED = 402,
+       HTTP_BAD_GATEWAY = 502,
+       HTTP_SERVICE_UNAVAILABLE = 503, /* overload, maintenance */
+       HTTP_RESPONSE_SETSIZE = 0xffffffff
+#endif
+};
+
+static const uint16_t http_response_type[] = {
+       HTTP_OK,
+       HTTP_MOVED_TEMPORARILY,
+       HTTP_REQUEST_TIMEOUT,
+       HTTP_NOT_IMPLEMENTED,
+#if ENABLE_FEATURE_HTTPD_BASIC_AUTH 
+       HTTP_UNAUTHORIZED,
+#endif
+       HTTP_NOT_FOUND,
+       HTTP_BAD_REQUEST,
+       HTTP_FORBIDDEN,
+       HTTP_INTERNAL_SERVER_ERROR,
+#if 0   /* not implemented */
+       HTTP_CREATED,
+       HTTP_ACCEPTED,
+       HTTP_NO_CONTENT,
+       HTTP_MULTIPLE_CHOICES,
+       HTTP_MOVED_PERMANENTLY,
+       HTTP_NOT_MODIFIED,
+       HTTP_BAD_GATEWAY,
+       HTTP_SERVICE_UNAVAILABLE,
+#endif
+};
+
+static const struct {
+       const char *name;
+       const char *info;
+} http_response[ARRAY_SIZE(http_response_type)] = {
+       { "OK", NULL },
+       { "Found", "Directories must end with a slash" }, /* ?? */
+       { "Request Timeout", "No request appeared within 60 seconds" },
+       { "Not Implemented", "The requested method is not recognized" },
+#if ENABLE_FEATURE_HTTPD_BASIC_AUTH
+       { "Unauthorized", "" },
+#endif
+       { "Not Found", "The requested URL was not found" },
+       { "Bad Request", "Unsupported method" },
+       { "Forbidden", ""  },
+       { "Internal Server Error", "Internal Server Error" },
+#if 0   /* not implemented */
+       { "Created" },
+       { "Accepted" },
+       { "No Content" },
+       { "Multiple Choices" },
+       { "Moved Permanently" },
+       { "Not Modified" },
+       { "Bad Gateway", "" },
+       { "Service Unavailable", "" },
+#endif
+};
+
 struct globals {
        int verbose;            /* must be int (used by getopt32) */
        smallint flg_deny_all;
@@ -167,6 +250,9 @@ struct globals {
 #define hdr_buf bb_common_bufsiz1
        char *hdr_ptr;
        int hdr_cnt;
+#if ENABLE_FEATURE_HTTPD_ERROR_PAGES
+       const char *http_error_page[ARRAY_SIZE(http_response_type)];
+#endif
 };
 #define G (*ptr_to_globals)
 #define verbose           (G.verbose          )
@@ -192,6 +278,7 @@ struct globals {
 #define iobuf             (G.iobuf            )
 #define hdr_ptr           (G.hdr_ptr          )
 #define hdr_cnt           (G.hdr_cnt          )
+#define http_error_page   (G.http_error_page  )
 #define INIT_G() do { \
        PTR_TO_GLOBALS = xzalloc(sizeof(G)); \
        USE_FEATURE_HTTPD_BASIC_AUTH(g_realm = "Web Server Authentication";) \
@@ -200,70 +287,13 @@ struct globals {
 } while (0)
 
 
-typedef enum {
-       HTTP_OK = 200,
-       HTTP_MOVED_TEMPORARILY = 302,
-       HTTP_BAD_REQUEST = 400,       /* malformed syntax */
-       HTTP_UNAUTHORIZED = 401, /* authentication needed, respond with auth hdr */
-       HTTP_NOT_FOUND = 404,
-       HTTP_FORBIDDEN = 403,
-       HTTP_REQUEST_TIMEOUT = 408,
-       HTTP_NOT_IMPLEMENTED = 501,   /* used for unrecognized requests */
-       HTTP_INTERNAL_SERVER_ERROR = 500,
-#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_NOT_MODIFIED = 304,
-       HTTP_PAYMENT_REQUIRED = 402,
-       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", NULL },
-       { HTTP_MOVED_TEMPORARILY, "Found", "Directories must end with a slash." },
-       { HTTP_REQUEST_TIMEOUT, "Request Timeout",
-               "No request appeared within a reasonable time period." },
-       { HTTP_NOT_IMPLEMENTED, "Not Implemented",
-               "The requested method is not recognized by this server." },
-#if ENABLE_FEATURE_HTTPD_BASIC_AUTH
-       { HTTP_UNAUTHORIZED, "Unauthorized", "" },
-#endif
-       { HTTP_NOT_FOUND, "Not Found",
-               "The requested URL was not found on this server." },
-       { HTTP_BAD_REQUEST, "Bad Request", "Unsupported method." },
-       { HTTP_FORBIDDEN, "Forbidden", "" },
-       { HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error",
-               "Internal Server Error" },
-#if 0                               /* not implemented */
-       { HTTP_CREATED, "Created" },
-       { HTTP_ACCEPTED, "Accepted" },
-       { HTTP_NO_CONTENT, "No Content" },
-       { HTTP_MULTIPLE_CHOICES, "Multiple Choices" },
-       { HTTP_MOVED_PERMANENTLY, "Moved Permanently" },
-       { HTTP_NOT_MODIFIED, "Not Modified" },
-       { HTTP_BAD_GATEWAY, "Bad Gateway", "" },
-       { HTTP_SERVICE_UNAVAILABLE, "Service Unavailable", "" },
-#endif
-};
 
 
 #define STRNCASECMP(a, str) strncasecmp((a), (str), sizeof(str)-1)
 
+/* Prototypes */
+static void send_file_and_exit(const char *url, int headers) ATTRIBUTE_NORETURN;
+
 static void free_llist(has_next_ptr **pptr)
 {
        has_next_ptr *cur = *pptr;
@@ -382,11 +412,13 @@ static int scan_ip_mask(const char *str, unsigned *ipp, unsigned *maskp)
  *    .ext:mime/type   # new mime type not compiled into httpd
  *    [adAD]:from      # ip address allow/deny, * for wildcard
  *    /path:user:pass  # username/password
+ *    Ennn:error.html  # error page for status nnn
  *
  * Any previous IP rules are discarded.
  * If the flag argument is not SUBDIR_PARSE then all /path and mime rules
  * are also discarded.  That is, previous settings are retained if flag is
  * SUBDIR_PARSE.
+ * Error pages are only parsed on the main config file.
  *
  * path   Path where to look for httpd.conf (without filename).
  * flag   Type of the parse request.
@@ -486,18 +518,6 @@ static void parse_conf(const char *path, int flag)
 
                if (*p0 == 'a')
                        *p0 = 'A';
-               else if (*p0 != 'D' && *p0 != 'A'
-#if ENABLE_FEATURE_HTTPD_BASIC_AUTH
-                        && *p0 != '/'
-#endif
-#if ENABLE_FEATURE_HTTPD_CONFIG_WITH_MIME_TYPES
-                        && *p0 != '.'
-#endif
-#if ENABLE_FEATURE_HTTPD_CONFIG_WITH_SCRIPT_INTERPR
-                        && *p0 != '*'
-#endif
-                       )
-                        continue;
                if (*p0 == 'A' || *p0 == 'D') {
                        /* storing current config IP line */
                        pip = xzalloc(sizeof(Htaccess_IP));
@@ -527,6 +547,30 @@ static void parse_conf(const char *path, int flag)
                        }
                        continue;
                }
+
+#if ENABLE_FEATURE_HTTPD_ERROR_PAGES
+               if (flag == FIRST_PARSE && *p0 == 'E') {
+                       int i;
+                       /* error status code */
+                       int status = atoi(++p0);
+                       /* c already points at the character following ':' in parse loop */
+                       // c = strchr(p0, ':'); c++;
+                       if (status < HTTP_CONTINUE) {
+                               bb_error_msg("config error '%s' in '%s'", buf, cf);
+                               continue;
+                       }
+
+                       /* then error page; find matching status */
+                       for (i = 0; i < ARRAY_SIZE(http_response_type); i++) {
+                               if (http_response_type[i] == status) {
+                                       http_error_page[i] = concat_path_file((*c == '/') ? NULL : home_httpd, c);
+                                       break;
+                               }
+                       }
+                       continue;
+               }
+#endif
+
 #if ENABLE_FEATURE_HTTPD_BASIC_AUTH
                if (*p0 == '/') {
                        /* make full path from httpd root / current_path / config_line_path */
@@ -813,24 +857,29 @@ static void log_and_exit(void)
  * IE will puke big-time if the headers are not sent in one packet and the
  * second packet is delayed for any reason.
  * responseNum - the result code to send.
- * Return result of write().
  */
-static void send_headers(HttpResponseNum responseNum)
+static void send_headers(int responseNum)
 {
        static const char RFC1123FMT[] ALIGN1 = "%a, %d %b %Y %H:%M:%S GMT";
 
        const char *responseString = "";
        const char *infoString = NULL;
        const char *mime_type;
+#if ENABLE_FEATURE_HTTPD_ERROR_PAGES
+       const char *error_page = 0;
+#endif
        unsigned i;
        time_t timer = time(0);
        char tmp_str[80];
        int len;
 
-       for (i = 0; i < ARRAY_SIZE(httpResponseNames); i++) {
-               if (httpResponseNames[i].type == responseNum) {
-                       responseString = httpResponseNames[i].name;
-                       infoString = httpResponseNames[i].info;
+       for (i = 0; i < ARRAY_SIZE(http_response_type); i++) {
+               if (http_response_type[i] == responseNum) {
+                       responseString = http_response[i].name;
+                       infoString = http_response[i].info;
+#if ENABLE_FEATURE_HTTPD_ERROR_PAGES
+                       error_page = http_error_page[i];
+#endif
                        break;
                }
        }
@@ -862,6 +911,20 @@ static void send_headers(HttpResponseNum responseNum)
                                (g_query ? g_query : ""));
        }
 
+#if ENABLE_FEATURE_HTTPD_ERROR_PAGES
+       if (error_page && !access(error_page, R_OK)) {
+               strcat(iobuf, "\r\n");
+               len += 2;
+
+               if (DEBUG)
+                       fprintf(stderr, "headers: '%s'\n", iobuf);
+               full_write(1, iobuf, len);
+               if (DEBUG)
+                       fprintf(stderr, "writing error page: '%s'\n", error_page);
+               return send_file_and_exit(error_page, FALSE);
+       }
+#endif
+
        if (ContentLength != -1) {    /* file */
                strftime(tmp_str, sizeof(tmp_str), RFC1123FMT, gmtime(&last_mod));
                len += sprintf(iobuf + len, "Last-Modified: %s\r\n%s %"OFF_FMT"d\r\n",
@@ -885,8 +948,8 @@ static void send_headers(HttpResponseNum responseNum)
        }
 }
 
-static void send_headers_and_exit(HttpResponseNum responseNum) ATTRIBUTE_NORETURN;
-static void send_headers_and_exit(HttpResponseNum responseNum)
+static void send_headers_and_exit(int responseNum) ATTRIBUTE_NORETURN;
+static void send_headers_and_exit(int responseNum)
 {
        send_headers(responseNum);
        log_and_exit();
@@ -1279,9 +1342,12 @@ static void send_cgi_and_exit(
 
 /*
  * Send a file response to a HTTP request, and exit
+ * 
+ * Parameters:
+ * const char *url    The requested URL (with leading /).
+ * headers            Don't send headers before if FALSE.
  */
-static void send_file_and_exit(const char *url) ATTRIBUTE_NORETURN;
-static void send_file_and_exit(const char *url)
+static void send_file_and_exit(const char *url, int headers)
 {
        static const char *const suffixTable[] = {
        /* Warning: shorter equivalent suffix in one line must be first */
@@ -1350,10 +1416,12 @@ static void send_file_and_exit(const char *url)
        if (f < 0) {
                if (DEBUG)
                        bb_perror_msg("cannot open '%s'", url);
-               send_headers_and_exit(HTTP_NOT_FOUND);
+               if (headers)
+                       send_headers_and_exit(HTTP_NOT_FOUND);
        }
 
-       send_headers(HTTP_OK);
+       if (headers)
+               send_headers(HTTP_OK);
 
        /* If you want to know about EPIPE below
         * (happens if you abort downloads from local httpd): */
@@ -1789,7 +1857,7 @@ static void handle_incoming_and_exit(const len_and_sockaddr *fromAddr)
         * }
         */
 
-       send_file_and_exit(tptr);
+       send_file_and_exit(tptr, TRUE);
 }
 
 /*