Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, PHP, and whatever else I happen to feel like hacking at.

Calling an external server from an Apache module

Jerry Stratton, March 1, 2008

This is the second in a series describing how to write an Apache module. If you haven’t yet read Creating a C Apache module on Mac OS X Leopard you might want to do so now.

This article will expand on the original module. The original module wasn’t very useful except as a shell. This one will actually do something.

  • It will only run when user-based authentication is requested (even though this version won’t actually ask for a username and password).
  • It will make a request to an external server for authorization to go ahead.
  • It will deny access if the external server tells it to.

The basic module

[toggle code]

  • /* authorize depending on yes/no response of external server */
  • #include "httpd.h"
  • #include "http_config.h"
  • static int externalAuthorizer(request_rec *request) {
    • return HTTP_SERVICE_UNAVAILABLE;
  • }
  • static void myRegisterHooks(apr_pool_t *p) {
    • ap_hook_check_user_id(externalAuthorizer, NULL, NULL, APR_HOOK_MIDDLE);
  • }
  • module AP_MODULE_DECLARE_DATA external_auth_module = {
    • STANDARD20_MODULE_STUFF,
    • NULL, /* create per-directory config structures */
    • NULL, /* merge per-directory config structures */
    • NULL, /* create per-server config structures */
    • NULL, /* merge per-server config structures */
    • NULL, /* command handlers */
    • myRegisterHooks /* register hooks */
  • };

In the previous module, I registered the function using ap_hook_fixups. That ran the module on every page request. This time, I’m using ap_hook_check_user_id. This will run the module only when some configuration file tells Apache to ask for a username and password.

Call this file mod_external_auth.c and then compile it:

  1. sudo apxs -c -i -a mod_external_auth.c
  2. sudo apachectl configtest
  3. sudo apachectl graceful

Remember that if you’re using a G5 or an Intel, you might need to add some extra options. See the previous article on apache modules. This is the last time I’ll reference that article, but if you need advice on using the Mac’s webserver, read it.

If you try to view web pages on your server, nothing should change (unless you try to view a page in a password-protected directory). That’s because ap_hook_check_user_id doesn’t run unless there’s something like a .htaccess file telling Apache that this directory is password-protected.

First, add a .htaccess file in your web site’s directory (Sites in Mac OS X, or most likely public_html in Linux). It should have only “require valid-user” in it.

Depending on your installation, that might be all you need to do. Type “curl --head http://127.0.0.1/~USER/”, where USER is your username on your computer. If it comes back with “503 Service Temporarily Unavailable” you’re set. If it doesn’t, you need to edit your httpd.conf file. On Mac OS X Leopard, it is in /etc/apache2/httpd.conf. You may find it easiest to add a new configuration file in your home directory somewhere, perhaps Documents/Apache, and call it “custom.conf”. At the bottom of your httpd.conf file, add:

  • Include /Users/USER/Documents/Apache/custom.conf

In your custom.conf, put:

[toggle code]

  • <Directory "/Users/USER/Sites/">
    • AllowOverride AuthConfig
  • </Directory>

Replace Sites with public_html if that’s where your user sites are stored. Replace /Users with wherever your user directories are stored.

Now, when you use curl to view your web page, you should get a 503 error. You should get the same thing if you view it in a graphical browser, something like “Service Temporarily Unavailable”.

You get that error because that’s what the module automatically returns: HTTP_SERVICE_UNAVAILABLE.

Only run when needed

There are different kinds of authentication in Apache. I only want this one to be called when I ask for it. Add “AuthType Hoboes” to the top of the .htaccess file. That will be the trigger to call this module. In the module, replace externalAuthorizer with:

[toggle code]

  • static int externalAuthorizer(request_rec *request) {
    • const char *current_auth;
    • //if this is not a HOBOES auth type, bail and let someone else handle it
    • current_auth = ap_auth_type(request);
    • if (!current_auth || strcasecmp(current_auth, "hoboes")) {
      • return DECLINED;
    • }
    • return HTTP_FORBIDDEN;
  • }

In the list of includes, add:

  • #include "http_core.h"

This version of the function checks what the current auth_type is; if it is not “hoboes” (and we let it be case-insensitive because that’s how .htaccess files are supposed to work), the function returns DECLINE, that is, it declines to handle authorization for this page. Some other module will have to handle it, or the page will fail in some way.

Compile the module. When you use curl to view your web site’s headers, it should now come back with “403 Forbidden”. If you remove the AuthType line from the .htaccess file, it should come back as “500 Internal Server Error”. If you change AuthType to something other than “hoboes”, such as “hoboesx”, it should come back as “401 Authorization Required”.

Constructing the authentication URL

Put AuthType back to hoboes.

Presumably (hopefully!) you have been doing this testing on a development server, such as your desktop Macintosh. Now, go to your real server and somewhere on it add a file called “authorization.php”. For now, just put “No” in it, and nothing else.

Add apr_strings.h to the list of your include files:

  • #include "apr_strings.h"

Add this function above the externalAuthorizer function:

[toggle code]

  • /* create a request to send client information to an authentication server */
  • static char *makeAuthRequest(request_rec *request) {
    • char *authRequest;
    • char *remote_ip;
    • char *uri;
    • remote_ip = request->connection->remote_ip;
    • uri = request->uri;
    • authRequest = apr_pstrcat(request->pool, "http://www.hoboes.com/authorization.php?ip=", remote_ip, "&page=", uri, "\n", NULL);
    • return authRequest;
  • }

This function is going to create the authorization request. Replace my URL (hoboes.com…) with the URL to your authorization.php file. The authorization request will send the server the IP of the client making the request, and the page it is making the request for. Of course, for the moment that won’t matter because authorization.php is always going to say “No”.

Now, call makeAuthRequest in externalAuthorizer. Here is the new externalAuthorizer, with the new or changed stuff emphasized:

[toggle code]

  • static int externalAuthorizer(request_rec *request) {
    • const char *current_auth;
    • char *authRequest;
    • //if this is not a HOBOES auth type, bail and let someone else handle it
    • current_auth = ap_auth_type(request);
    • if (!current_auth || strcasecmp(current_auth, "hoboes")) {
      • return DECLINED;
    • }
    • //create authorization request
    • authRequest = makeAuthRequest(request);
    • apr_table_set(request->headers_out, "Location", authRequest);
    • return HTTP_MOVED_TEMPORARILY;
  • }

Note that the HTTP_FORBIDDEN has been replaced with HTTP_MOVED_TEMPORARILY. If you go to http://127.0.0.1/~USER/ in your browser, you should be redirected to your authorization.php file, which will just say “No”.

Authenticate!

Now comes the fun part. In makeAuthRequest, replace the authorization.php apr_pstrcat line with:

  • authRequest = apr_pstrcat(request->pool, "GET /authorization.php?ip=", remote_ip, "&page=", uri, "\n", NULL);

Why? Because the next step is to make a function that calls that URL, and as far as I can tell there is no “apr_get_url” to call a complete URL and get the response. The module will have to carefully construct a “socket” to the server and make the file request separately.

Add an include that makes logging easier:

  • #include "http_log.h"

In order to log errors, add a log function:

[toggle code]

  • static int logError(apr_status_t status, char *message, request_rec *request) {
    • ap_log_rerror(APLOG_MARK, APLOG_ERR, status, request, "external_auth: error %s", message);
    • return HTTP_SERVICE_UNAVAILABLE;
  • }

The ap_log_error function will put an error message in the server’s error log, often in /var/log/apache2/error_log.

Now comes the big function:

[toggle code]

  • static int authenticate(request_rec *request, char *authRequest, char response[]) {
    • apr_socket_t *sock;
    • apr_sockaddr_t *sockaddr;
    • apr_status_t status;
    • //timeout appears to be in microseconds, so time out after 5 seconds
    • apr_interval_time_t timeout = 5000000;
    • apr_size_t item_size;
    • if ((status = apr_sockaddr_info_get(&sockaddr, "216.92.252.156", APR_INET, 80, 0, request->pool)) != APR_SUCCESS) {
      • return logError(status, "creating socket address", request);
    • }
    • if ((status = apr_socket_create(&sock, sockaddr->family, SOCK_STREAM, APR_PROTO_TCP, request->pool)) != APR_SUCCESS) {
      • return logError(status, "creating socket", request);
    • }
    • if ((status = apr_socket_timeout_set(sock, timeout)) != APR_SUCCESS) {
      • return logError(status, "setting socket timeout", request);
    • }
    • if ((status = apr_socket_connect(sock, sockaddr)) != APR_SUCCESS) {
      • return logError(status, "connecting", request);
    • }
    • item_size = strlen(authRequest);
    • if ((status = apr_socket_send(sock, authRequest, &item_size)) != APR_SUCCESS) {
      • return logError(status, "sending query", request);
    • }
    • item_size = sizeof(response);
    • if ((status = apr_socket_recv(sock, response, &item_size)) != APR_SUCCESS) {
      • return logError(status, "receiving response", request);
    • }
    • apr_socket_close(sock);
    • /* make sure it ends, and chop off carriage return */
    • response[item_size] = '\0';
    • if (item_size > 0) {
      • if (response[item_size-1] == '\n') {
        • response[item_size-1] = '\0';
      • }
    • }
    • return APR_SUCCESS;
  • }

This function:

  1. Creates an Internet socket to the specified host;
  2. Sets the timeout to five seconds;
  3. Connects to the socket;
  4. Sends the authentication request;
  5. Gets the authentication response.

If any of those steps run into an error, it returns an error 503 and logs it.

Finally, externalAuthorizer needs to call the authenticate function and parse the response:

[toggle code]

  • static int externalAuthorizer(request_rec *request) {
    • const char *current_auth;
    • char *authRequest;
    • /* more characters than we will ever need for the auth server's response */
    • char response[5000];
    • int status;
    • //if this is not a HOBOES auth type, bail and let someone else handle it
    • current_auth = ap_auth_type(request);
    • if (!current_auth || strcasecmp(current_auth, "hoboes")) {
      • return DECLINED;
    • }
    • //create authorization request
    • authRequest = makeAuthRequest(request);
    • //make call to authentication server
    • if ((status = authenticate(request, authRequest, response)) != APR_SUCCESS) {
      • return status;
    • }
    • //parse authentication response
    • if (strcmp("No", response) == 0) {
      • return HTTP_FORBIDDEN;
    • } else if (strcmp("Yes", response) == 0) {
      • return OK;
    • } else {
      • return HTTP_SERVICE_UNAVAILABLE;
    • }
  • }

I’ve emphasized the new/changed lines.

Now when you curl your web page or view it in a browser, you should see an error 403 Forbidden. If you switch authorize.php on the external server to say “Yes” instead of “No”, pages should now be allowed. And if you make it reply other than Yes or No, it will go back to error 503.

Further tests

You might test making it dynamic: refuse authorization except during certain parts of the day, or allow authorization to a specific list of client IP addresses. A simple PHP script might look like this:

[toggle code]

  • <?
    • $ip = $_GET['ip'];
    • $page = $_GET['page'];
    • $server = $_SERVER['REMOTE_ADDR'];
    • $stamp = date('r');
    • if (in_array($ip, array('127.0.0.1', '168.6.6.6')) {
      • $response = "Yes";
    • } else {
      • $response = "No";
    • }
    • echo $response;
    • file_put_contents("/users/myhome/logs/authorization.txt", "$stamp: $response; IP: $ip; Server: $server; Page: $page\n", FILE_APPEND);
  • ?>

You will also want to test the error log functionality by putting a bad IP address in the source code (such as adding an “x” to one of the numbers) or disconnecting yourself from the Internet. You should get back a 503 error, and the error_log file should contain an error from the module.

Note that while this module might actually have some uses, remember that the hook hooks it into user authentication. If you don’t use it for user authentication, you’ll want to research what the correct hook is for the kind of authentication you’re doing.

  1. <- Less Design is More
  2. Speaking Geek ->