httpd-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Alexei Kosut <ako...@organic.com>
Subject mod_isapi
Date Tue, 22 Jul 1997 01:10:45 GMT
I sent this to the list a week ago, but have seen no (or little)
response, so I'll send it again; the first time was buried in a larger
email about something else, people may not have read it.

Enclosed is a module I wrote that allows ISAPI Extensions (aka ISAs) to
be run on Windows with Apache. Internet Server Applications are DLLs that
use the ISAPI (Internet Server API) to talk to a web server. They
basically function as a CGI replacement, and do little else. This module
implements all of the ISAPI 2.0 specification except for a few
Microsoft-specific extensions that IIS has, mostly allowing for
asynchronous I/O. Since Apache's I/O is not multithreaded as IIS' is,
this is nearly impossible with Apache. Other than that, the entire spec
is implemented, and the number of ISAs that I've tried seem to work.

I'd like to see this module tossed in nt/ and included with Apache 1.3
(maybe even 1.3a1). ISAPI support is one part of getting Apache used in
the Windows world, as it allows them to use any ISAs they might be used
to using with Windows servers. So I think we should include it as part of
the default Apache installation on Windows

A few more technical notes: There is one other deviation from the ISAPI
spec; IIS puts only 48k of any input data into the
EXTENSION_CONTROL_BLOCK structure; the rest must be read with
ReadClient(). Apache (like WebSite) reads in all the incoming data and
makes it all available. It's easier, and seems to work.

IIS, being multithreaded through and through, loads an ISA the first time
it is called, and keeps it loaded until it decides it needs to get rid of
the memory, and unloads it. Apache has no mechanism to do that properly,
so I have the module load and unload the DLL for each request. It's still
faster than CGI.

Also note that while this module does implement the ISAPI Extension spec,
it does not do ISAPI Filters. Filters are more like Apache modules, they
are loaded at config time, and can do things like map URLs to filenames,
processes headers, handle logging. They can also do things like
encryption. Implementing ISAPi Filters would be, IMHO, more trouble than
it would be worth, and would probably involve a lot of hacking into
Apache's core.

Another issue is security: I have it currently so that ISAs have the same
access restrictions as CGIs (Options ExecCGI). This makes sense to me;
they do similar things, and are both basically executable
content. Although ISAs are loaded into the server's process space, while
CGIs are not, they cannot access the Apache API, and so they are just as
safe. Although a poorly-programmed or malicious ISA can crash its parent
process, so can a CGI, and Apache will spawn more children.

Anyhow, here's mod_isapi.c:

/* ====================================================================
 * Copyright (c) 1995-1997 The Apache Group.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission.
 *
 * 5. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE APACHE GROUP OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Group and was originally based
 * on public domain software written at the National Center for
 * Supercomputing Applications, University of Illinois, Urbana-Champaign.
 * For more information on the Apache Group and the Apache HTTP server
 * project, please see <http://www.apache.org/>.
 *
 */

/*
 * mod_isapi.c - Internet Server Application (ISA) module for Apache
 * by Alexei Kosut <akosut@apache.org>
 *
 * This module implements Microsoft's ISAPI, allowing Apache (when running
 * under Windows) to load Internet Server Applications (ISAPI extensions).
 * It implements all of the ISAPI 2.0 specification, except for the 
 * "Microsoft-only" extensions dealing with asynchronous I/O. All ISAPI
 * extensions that use only synchronous I/O and are compatible with the
 * ISAPI 2.0 specification should work (most ISAPI 1.0 extensions should
 * function as well).
 *
 * To load, simply place the ISA in a location in the document tree.
 * Then add an "AddHandler isapi-isa dll" into your config file.
 * You should now be able to load ISAPI DLLs just be reffering to their
 * URLs. Make sure the ExecCGI option is active in the directory
 * the ISA is in.
 */

#include "../httpd.h"
#include "../http_config.h"
#include "../http_core.h"
#include "../http_protocol.h"
#include "../http_request.h"
#include "../http_log.h"
#include "../util_script.h"

/* We use the exact same header file as the original */
#include <HttpExt.h>

module isapi_module;

/* Our "Connection ID" structure */

typedef struct {
    LPEXTENSION_CONTROL_BLOCK ecb;
    request_rec *r;
    int status;
} isapi_cid;

/* Declare the ISAPI functions */

BOOL WINAPI GetServerVariable (HCONN hConn, LPSTR lpszVariableName,
			       LPVOID lpvBuffer, LPDWORD lpdwSizeofBuffer);
BOOL WINAPI WriteClient (HCONN ConnID, LPVOID Buffer, LPDWORD lpwdwBytes,
			 DWORD dwReserved);
BOOL WINAPI ReadClient (HCONN ConnID, LPVOID lpvBuffer, LPDWORD lpdwSize);
BOOL WINAPI ServerSupportFunction (HCONN hConn, DWORD dwHSERequest,
				   LPVOID lpvBuffer, LPDWORD lpdwSize,
				   LPDWORD lpdwDataType);

int isapi_handler (request_rec *r) {
    LPEXTENSION_CONTROL_BLOCK ecb =
	pcalloc(r->pool, sizeof(struct _EXTENSION_CONTROL_BLOCK));
    HSE_VERSION_INFO *pVer = pcalloc(r->pool, sizeof(HSE_VERSION_INFO));

    HINSTANCE isapi_handle;
    BOOL (*isapi_version)(HSE_VERSION_INFO *); /* entry point 1 */
    DWORD (*isapi_entry)(LPEXTENSION_CONTROL_BLOCK); /* entry point 2 */
    BOOL (*isapi_term)(DWORD); /* optional entry point 3 */

    isapi_cid *cid = pcalloc(r->pool, sizeof(isapi_cid));
    table *e = r->subprocess_env;
    int retval;

    /* Use similar restrictions as CGIs */

    if (!(allow_options(r) & OPT_EXECCGI))
	return FORBIDDEN;

    if (S_ISDIR(r->finfo.st_mode))
	return FORBIDDEN;

    if (r->finfo.st_mode == 0)
	return NOT_FOUND;

    /* Load the module */

    if (!(isapi_handle = LoadLibraryEx(r->filename, NULL,
				       LOAD_WITH_ALTERED_SEARCH_PATH))) {
	log_reason("Could not load DLL", r->filename, r);
	return SERVER_ERROR;
    }

    if (!(isapi_version =
	  (void *)(GetProcAddress(isapi_handle, "GetExtensionVersion")))) {
	log_reason("DLL could not load GetExtensionVersion()", r->filename, r);
	FreeLibrary(isapi_handle);
	return SERVER_ERROR;
    }

    if (!(isapi_entry =
	  (void *)(GetProcAddress(isapi_handle, "HttpExtensionProc")))) {
	log_reason("DLL could not load HttpExtensionProc()", r->filename, r);
	FreeLibrary(isapi_handle);
	return SERVER_ERROR;
    }

    isapi_term = (void *)(GetProcAddress(isapi_handle, "TerminateExtension"));

    /* Run GetExtensionVersion() */

    if ((*isapi_version)(pVer) != TRUE) {
	log_reason("ISAPI GetExtensionVersion() failed", r->filename, r);
	FreeLibrary(isapi_handle);
	return SERVER_ERROR;
    }

    /* Set up variables */
    add_common_vars(r);
    add_cgi_vars(r);

    /* Set up connection ID */
    ecb->ConnID = (HCONN)cid;
    cid->ecb = ecb;
    cid->r = r;
    cid->status = 0;

    ecb->cbSize = sizeof(struct _EXTENSION_CONTROL_BLOCK);
    ecb->dwVersion = MAKELONG(0, 2);
    ecb->dwHttpStatusCode = 0;
    strcpy(ecb->lpszLogData, "");
    ecb->lpszMethod = r->method;
    ecb->lpszQueryString = table_get(e, "QUERY_STRING");
    ecb->lpszPathInfo = table_get(e, "PATH_INFO");
    ecb->lpszPathTranslated = table_get(e, "PATH_TRANSLATED");
    ecb->lpszContentType = table_get(e, "CONTENT_TYPE");

    /* Set up client input */
    if ((retval = setup_client_block(r, REQUEST_CHUNKED_ERROR))) {
	if (isapi_term) (*isapi_term)(HSE_TERM_MUST_UNLOAD);
	FreeLibrary(isapi_handle);
	return retval;
    }

    if (should_client_block(r)) {
	/* Unlike IIS, which limits this to 48k, we read the whole
	 * sucker in. I suppose this could be bad for memory if someone
	 * uploaded the complete works of Shakespear. Well, WebSite
	 * does the same thing.
	 */
	long to_read = atol(table_get(e, "CONTENT_LENGTH"));
	long read;

	ecb->lpbData = pcalloc(r->pool, 1 + to_read);

	if ((read = get_client_block(r, ecb->lpbData, to_read)) < 0) {
	    if (isapi_term) (*isapi_term)(HSE_TERM_MUST_UNLOAD);
	    FreeLibrary(isapi_handle);
	    return SERVER_ERROR;
	}

	/* Although its not to spec, IIS seems to null-terminate
	 * its lpdData string. So we will too. To make sure
	 * cbAvailable matches cbTotalBytes, we'll up the latter
	 * and equalize them.
	 */
	ecb->cbAvailable = ecb->cbTotalBytes = read + 1;
	ecb->lpbData[read] = '\0';
	}
    else {
	ecb->cbTotalBytes = 0;
	ecb->cbAvailable = 0;
	ecb->lpbData = NULL;
    }

    /* Set up the callbacks */

    ecb->GetServerVariable = &GetServerVariable;
    ecb->WriteClient = &WriteClient;
    ecb->ReadClient = &ReadClient;
    ecb->ServerSupportFunction = &ServerSupportFunction;

    /* All right... try and load the sucker */
    retval = (*isapi_entry)(ecb);

    /* Set the status (for logging) */
    if (ecb->dwHttpStatusCode)
	r->status = ecb->dwHttpStatusCode;

    /* Check for a log message - and log it */
    if (ecb->lpszLogData && strcmp(ecb->lpszLogData, ""))
	log_reason(ecb->lpszLogData, r->filename, r);

    /* All done with the DLL... get rid of it */
    if (isapi_term) (*isapi_term)(HSE_TERM_MUST_UNLOAD);
    FreeLibrary(isapi_handle);

    switch(retval) {
    case HSE_STATUS_SUCCESS:
    case HSE_STATUS_SUCCESS_AND_KEEP_CONN:
	/* Ignore the keepalive stuff; Apache handles it just fine without
	 * the ISA's "advice".
	 */

	if (cid->status) /* We have a special status to return */
	    return cid->status;

	return OK;
    case HSE_STATUS_PENDING:	/* We don't support this */
	log_reason("ISAPI asynchronous I/O not supported", r->filename, r);
    case HSE_STATUS_ERROR:
    default:
	return SERVER_ERROR;
    }

}

BOOL WINAPI GetServerVariable (HCONN hConn, LPSTR lpszVariableName,
			       LPVOID lpvBuffer, LPDWORD lpdwSizeofBuffer) {
    request_rec *r = ((isapi_cid *)hConn)->r;
    table *e = r->subprocess_env;
    char *result;
    
    /* Mostly, we just grab it from the environment, but there are
     * a couple of special cases
     */

    if (!strcasecmp(lpszVariableName, "UNMAPPED_REMOTE_USER")) {
	/* We don't support NT users, so this is always the same as
	 * REMOTE_USER
	 */
	result = table_get(e, "REMOTE_USER");
    }
    else if (!strcasecmp(lpszVariableName, "SERVER_PORT_SECURE")) {
	/* Apache doesn't support secure requests inherently, so
	 * we have no way of knowing. We'll be conservative, and say
	 * all requests are insecure.
	 */
	result = "0";
    }
    else if (!strcasecmp(lpszVariableName, "URL")) {
	result = r->uri;
    }
    else {
	result = table_get(e, lpszVariableName);
    }

    if (result) {
	if (strlen(result) > *lpdwSizeofBuffer) {
	    *lpdwSizeofBuffer = strlen(result);
	    SetLastError(ERROR_INSUFFICIENT_BUFFER);
	    return FALSE;
	}
	strncpy(lpvBuffer, result, *lpdwSizeofBuffer);
	return TRUE;
    }

    /* Didn't find it */
    SetLastError(ERROR_INVALID_INDEX);
    return FALSE;
}

BOOL WINAPI WriteClient (HCONN ConnID, LPVOID Buffer, LPDWORD lpwdwBytes,
			 DWORD dwReserved) {
    request_rec *r = ((isapi_cid *)ConnID)->r;
    int writ;	/* written, actually, but why shouldn't I make up words? */

    /* We only support synchronous writing */
    if (dwReserved && dwReserved != HSE_IO_SYNC) {
	log_reason("ISAPI asynchronous I/O not supported", r->filename, r);
	SetLastError(ERROR_INVALID_PARAMETER);
	return FALSE;
    }

    if ((writ = rwrite(Buffer, *lpwdwBytes, r)) == EOF) {
	SetLastError(ERROR); /* XXX: Find the right error code */
	return FALSE;
    }
   
    *lpwdwBytes = writ;
    return TRUE;
}

BOOL WINAPI ReadClient (HCONN ConnID, LPVOID lpvBuffer, LPDWORD lpdwSize) {
    /* Doesn't need to do anything; we've read all the data already */
    return TRUE;
}

BOOL WINAPI ServerSupportFunction (HCONN hConn, DWORD dwHSERequest,
				   LPVOID lpvBuffer, LPDWORD lpdwSize,
				   LPDWORD lpdwDataType) {
    isapi_cid *cid = (isapi_cid *)hConn;
    request_rec *subreq, *r = cid->r;
    char *data;

    switch (dwHSERequest) {
    case HSE_REQ_SEND_URL_REDIRECT_RESP:
	/* Set the status to be returned when the HttpExtensionProc()
	 * is done.
	 */
	table_set (r->headers_out, "Location", lpvBuffer);
	cid->status = cid->r->status = cid->ecb->dwHttpStatusCode = REDIRECT;
	return TRUE;

    case HSE_REQ_SEND_URL:
	/* Read any additional input */

	if (r->remaining > 0) {
	    char argsbuffer[HUGE_STRING_LEN];

	    while (get_client_block(r, argsbuffer, HUGE_STRING_LEN));
	}
	
	/* Reset the method to GET */
	r->method = pstrdup(r->pool, "GET");
	r->method_number = M_GET;

	/* Don't let anyone think there's still data */
	table_unset(r->headers_in, "Content-Length");
	
	internal_redirect((char *)lpvBuffer, r);	
	return TRUE;

    case HSE_REQ_SEND_RESPONSE_HEADER:
	r->status_line = lpvBuffer ? lpvBuffer : pstrdup(r->pool, "200 OK");
	sscanf(r->status_line, "%d", &r->status);
	cid->ecb->dwHttpStatusCode = r->status;

	/* Now fill in the HTTP headers, and the rest of it. Ick.
	 * lpdwDataType contains a string that has headers (in MIME
	 * format), a blank like, then (possibly) data. We need
	 * to parse it.
	 *
	 * Easy case first:
	 */
	if (!lpdwDataType) {
	    send_http_header(r);
	    return TRUE;
	}

	/* Make a copy - don't disturb the original */
	data = pstrdup(r->pool, (char *)lpdwDataType);

	/* We *should* break before this while loop ends */
	while (*data) {
	    char *value, *lf = strchr(data, '\n');
	    int p;

	    if (!lf) { /* Huh? Invalid data, I think */
		log_reason("ISA sent invalid headers", r->filename, r);
		SetLastError(ERROR);	/* XXX: Find right error */
		return FALSE;
	    }

	    /* Get rid of \n and \r */
	    *lf = '\0';
	    p = strlen(data);
	    if (p > 0 && data[p-1] == '\r') data[p-1] = '\0';
	    
	    /* End of headers */
	    if (*data == '\0') {
		data = lf + 1;	/* Reset data */
		break;
	    }

	    if (!(value = strchr(data, ':'))) {
		SetLastError(ERROR);	/* XXX: Find right error */
		log_reason("ISA sent invalid headers", r->filename, r);
		return FALSE;
	    }

	    *value++ = '\0';
	    while (*value && isspace(*value)) ++value;

	    /* Check all the special-case headers. Similar to what
	     * scan_script_header() does (see that function for
	     * more detail)
	     */

	    if (!strcasecmp(data, "Content-Type")) {
		/* Nuke trailing whitespace */
		
		char *endp = value + strlen(value) - 1;
		while (endp > value && isspace(*endp)) *endp-- = '\0';
            
		r->content_type = pstrdup (r->pool, value);
	    }
	    else if (!strcasecmp(data, "Content-Length")) {
		table_set(r->headers_out, data, value);
	    }
	    else if (!strcasecmp(data, "Transfer-Encoding")) {
		table_set(r->headers_out, data, value);
	    }
	    else if (!strcasecmp(data, "Set-Cookie")) {
		table_add(r->err_headers_out, data, value);
	    }
	    else {
		table_merge(r->err_headers_out, data, value);
	    }
	  
	    /* Reset data */
	    data = lf + 1;
	}
	
	/* All the headers should be set now */

	send_http_header(r);

	/* Any data left should now be sent directly */
	rputs(data, r);

	return TRUE;

    case HSE_REQ_MAP_URL_TO_PATH:
	/* Map a URL to a filename */
	subreq = sub_req_lookup_uri(pstrndup(r->pool, (char *)lpvBuffer,
					      *lpdwSize), r);

	GetFullPathName(subreq->filename, *lpdwSize - 1, (char *)lpvBuffer, NULL);

	/* IIS puts a trailing slash on directories, Apache doesn't */

	if (S_ISDIR (subreq->finfo.st_mode)) {
		int l = strlen((char *)lpvBuffer);

		((char *)lpvBuffer)[l] = '\\';
		((char *)lpvBuffer)[l + 1] = '\0';
	}
	
	return TRUE;

    case HSE_REQ_DONE_WITH_SESSION:
	/* Do nothing... since we don't support async I/O, they'll
	 * return from HttpExtensionProc soon
	 */
	return TRUE;

    /* We don't support all this async I/O, Microsoft-specific stuff */
    case HSE_REQ_IO_COMPLETION:
    case HSE_REQ_TRANSMIT_FILE:
	log_reason("ISAPI asynchronous I/O not supported", r->filename, r);
    default:
	SetLastError(ERROR_INVALID_PARAMETER);
	return FALSE;
    }
}

handler_rec isapi_handlers[] = {
{ "isapi-isa", isapi_handler },
{ NULL}
};

module isapi_module = {
   STANDARD_MODULE_STUFF,
   NULL,			/* initializer */
   NULL,			/* create per-dir config */
   NULL,			/* merge per-dir config */
   NULL,			/* server config */
   NULL,			/* merge server config */
   NULL,			/* command table */
   isapi_handlers,	       	/* handlers */
   NULL,			/* filename translation */
   NULL,			/* check_user_id */
   NULL,			/* check auth */
   NULL,			/* check access */
   NULL,			/* type_checker */
   NULL,			/* logger */
   NULL				/* header parser */
};


Mime
View raw message