# HG changeset patch # User Maxim Dounin # Date 1266665430 -10800 # Node ID 436da5355bd5f50d680e3ef094bfa40138443203 Auth request module. diff --git a/LICENSE b/LICENSE new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2010 Maxim Dounin + * + * 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. + * + * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS 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 AUTHOR OR 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. + */ diff --git a/README b/README new file mode 100644 --- /dev/null +++ b/README @@ -0,0 +1,34 @@ +Auth request module for nginx. + +This module allows authorization based on subrequest result. Once subrequest +returns 2xx status - access is allowed, on 401 or 403 - disabled with +appropriate status. Anything else is considered to be an error. + +For 401 status WWW-Authenticate header from subrequest response will be +passed to client. + +Configuration directives: + + auth_request |off + + Context: http, server, location + Default: off + + Switches auth request module on and sets uri which will be asked for + authorization. + +Usage: + + location /private/ { + auth_request /auth; + ... + } + + location = /auth { + proxy_pass ... + } + +To compile nginx with auth request module, use "--add-module " option +to nginx configure. + +Development of this module was sponsored by Openstat (http://www.openstat.com/). diff --git a/config b/config new file mode 100644 --- /dev/null +++ b/config @@ -0,0 +1,10 @@ +# (C) Maxim Dounin +# Configuration for ngx_http_auth_request_module. + +ngx_addon_name="ngx_http_auth_request_module" + +HTTP_MODULES="$HTTP_MODULES \ + ngx_http_auth_request_module" + +NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ + $ngx_addon_dir/ngx_http_auth_request_module.c" diff --git a/ngx_http_auth_request_module.c b/ngx_http_auth_request_module.c new file mode 100644 --- /dev/null +++ b/ngx_http_auth_request_module.c @@ -0,0 +1,233 @@ + +/* + * Copyright (C) Maxim Dounin + */ + + +#include +#include +#include + + +typedef struct { + ngx_str_t uri; +} ngx_http_auth_request_conf_t; + +typedef struct { + ngx_uint_t done; + ngx_uint_t status; + ngx_http_request_t *subrequest; +} ngx_http_auth_request_ctx_t; + + +static ngx_int_t ngx_http_auth_request_handler(ngx_http_request_t *r); +static ngx_int_t ngx_http_auth_request_done(ngx_http_request_t *r, + void *data, ngx_int_t rc); +static void *ngx_http_auth_request_create_conf(ngx_conf_t *cf); +static char *ngx_http_auth_request_merge_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_http_auth_request_init(ngx_conf_t *cf); + + +static ngx_command_t ngx_http_auth_request_commands[] = { + + { ngx_string("auth_request"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_auth_request_conf_t, uri), + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_auth_request_module_ctx = { + NULL, /* preconfiguration */ + ngx_http_auth_request_init, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_http_auth_request_create_conf, /* create location configuration */ + ngx_http_auth_request_merge_conf /* merge location configuration */ +}; + + +ngx_module_t ngx_http_auth_request_module = { + NGX_MODULE_V1, + &ngx_http_auth_request_module_ctx, /* module context */ + ngx_http_auth_request_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_int_t +ngx_http_auth_request_handler(ngx_http_request_t *r) +{ + ngx_table_elt_t *h, *ho; + ngx_http_request_t *sr; + ngx_http_post_subrequest_t *ps; + ngx_http_auth_request_ctx_t *ctx; + ngx_http_auth_request_conf_t *arcf; + + arcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_request_module); + + if (arcf->uri.len == 0) { + return NGX_DECLINED; + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "auth request handler"); + + ctx = ngx_http_get_module_ctx(r, ngx_http_auth_request_module); + + if (ctx != NULL) { + if (!ctx->done) { + return NGX_AGAIN; + } + + if (ctx->status == NGX_HTTP_FORBIDDEN) { + return ctx->status; + } + + if (ctx->status == NGX_HTTP_UNAUTHORIZED) { + sr = ctx->subrequest; + + h = sr->headers_out.www_authenticate; + + if (!h && sr->upstream) { + h = sr->upstream->headers_in.www_authenticate; + } + + if (h) { + ho = ngx_list_push(&r->headers_out.headers); + if (ho == NULL) { + return NGX_ERROR; + } + + *ho = *h; + + r->headers_out.www_authenticate = ho; + } + + return ctx->status; + } + + if (ctx->status >= NGX_HTTP_OK + && ctx->status < NGX_HTTP_SPECIAL_RESPONSE) + { + return NGX_OK; + } + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "auth request unexpected status: %d", ctx->status); + + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_request_ctx_t)); + if (ctx == NULL) { + return NGX_ERROR; + } + + ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); + if (ps == NULL) { + return NGX_ERROR; + } + + ps->handler = ngx_http_auth_request_done; + ps->data = ctx; + + if (ngx_http_subrequest(r, &arcf->uri, NULL, &sr, ps, + NGX_HTTP_SUBREQUEST_WAITED) + != NGX_OK) + { + return NGX_ERROR; + } + + sr->header_only = 1; + + ctx->subrequest = sr; + + ngx_http_set_ctx(r, ctx, ngx_http_auth_request_module); + + return NGX_AGAIN; +} + + +static ngx_int_t +ngx_http_auth_request_done(ngx_http_request_t *r, void *data, ngx_int_t rc) +{ + ngx_http_auth_request_ctx_t *ctx = data; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "auth request done s:%d", r->headers_out.status); + + ctx->done = 1; + ctx->status = r->headers_out.status; + + return rc; +} + + +static void * +ngx_http_auth_request_create_conf(ngx_conf_t *cf) +{ + ngx_http_auth_request_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_request_conf_t)); + if (conf == NULL) { + return NULL; + } + + /* + * set by ngx_pcalloc(): + * + * conf->uri.len = { 0, NULL }; + */ + + return conf; +} + + +static char * +ngx_http_auth_request_merge_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_http_auth_request_conf_t *prev = parent; + ngx_http_auth_request_conf_t *conf = child; + + ngx_conf_merge_str_value(conf->uri, prev->uri, ""); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_http_auth_request_init(ngx_conf_t *cf) +{ + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_auth_request_handler; + + return NGX_OK; +} diff --git a/t/auth-request.t b/t/auth-request.t new file mode 100644 --- /dev/null +++ b/t/auth-request.t @@ -0,0 +1,163 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for auth request module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite proxy fastcgi auth_basic/) + ->plan(13); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +master_process off; +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + return 444; + } + + location /open { + auth_request /auth-open; + } + location = /auth-open { + return 204; + } + + location /open-static { + auth_request /auth-open-static; + } + location = /auth-open-static { + # nothing, use static file + } + + location /unauthorized { + auth_request /auth-unauthorized; + } + location = /auth-unauthorized { + return 401; + } + + location /forbidden { + auth_request /auth-forbidden; + } + location = /auth-forbidden { + return 403; + } + + location /error { + auth_request /auth-error; + } + location = /auth-error { + return 404; + } + + location /proxy { + auth_request /auth-proxy; + } + location = /auth-proxy { + proxy_pass http://127.0.0.1:8080/auth-basic; + } + location = /auth-basic { + auth_basic "restricted"; + auth_basic_user_file %%TESTDIR%%/htpasswd; + } + + location /fastcgi { + auth_request /auth-fastcgi; + } + location = /auth-fastcgi { + fastcgi_pass 127.0.0.1:8081; + } + } +} + +EOF + +$t->write_file('htpasswd', 'user:zz1T8N4tWvmbE' . "\n"); +$t->write_file('auth-basic', 'INVISIBLE'); +$t->write_file('auth-open-static', 'INVISIBLE'); +$t->run(); + +############################################################################### + +pass('runs'); + +like(http_get('/open'), qr/ 404 /, 'auth open'); +like(http_get('/unauthorized'), qr/ 401 /, 'auth unauthorized'); +like(http_get('/forbidden'), qr/ 403 /, 'auth forbidden'); +like(http_get('/error'), qr/ 500 /, 'auth error'); + +like(http_get('/open-static'), qr/ 404 /, 'auth open static'); +unlike(http_get('/open-static'), qr/INVISIBLE/, 'auth static no content'); + +like(http_get('/proxy'), qr/ 401 /, 'proxy auth unauthorized'); +like(http_get('/proxy'), qr/WWW-Authenticate: Basic realm="restricted"/, + 'proxy auth has www-authenticate'); +like(http_get_auth('/proxy'), qr/ 404 /, 'proxy auth pass'); +unlike(http_get_auth('/proxy'), qr/INVISIBLE/, 'proxy auth no content'); + +SKIP: { + eval { require FCGI; }; + skip 'FCGI not installed', 2 if $@; + + $t->run_daemon(\&fastcgi_daemon); + $t->waitforsocket('127.0.0.1:8081'); + + like(http_get('/fastcgi'), qr/ 404 /, 'fastcgi auth open'); + unlike(http_get('/fastcgi'), qr/INVISIBLE/, 'fastcgi auth no content'); +} + +############################################################################### + +sub http_get_auth { + my ($url, %extra) = @_; + return http(<Accept() >= 0) { + print <