commit eaa5cfef6f94d56344f7bbde02c8c76dacf4d422 Author: R. Tyler Croy Date: Fri May 30 16:15:20 2014 -0700 Initial open source commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..897fae4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +tmp +.rvmrc +Gemfile.lock +logs +build +t/servroot diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..16d3933 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "contrib/nginx"] + path = contrib/nginx + url = https://github.com/nginx/nginx.git +[submodule "contrib/ngx_devel_kit"] + path = contrib/ngx_devel_kit + url = https://github.com/simpl/ngx_devel_kit.git +[submodule "contrib/lua-nginx-module"] + path = contrib/lua-nginx-module + url = https://github.com/chaoslawful/lua-nginx-module.git +[submodule "contrib/memc-nginx-module"] + path = contrib/memc-nginx-module + url = https://github.com/agentzh/memc-nginx-module.git +[submodule "contrib/echo-nginx-module"] + path = contrib/echo-nginx-module + url = https://github.com/agentzh/echo-nginx-module.git +[submodule "contrib/headers-more-nginx-module"] + path = contrib/headers-more-nginx-module + url = https://github.com/agentzh/headers-more-nginx-module.git diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..4f9659d --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org/' + +gem 'sinatra' +gem 'httparty' +gem 'god' +gem 'haml' +# For reloading every request to the sinatra test apps +gem 'shotgun' diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e9fbb81 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Lookout, Inc + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e672750 --- /dev/null +++ b/Makefile @@ -0,0 +1,149 @@ +# Debian package name & version +MAJOR_VER=0 +MINOR_VER=1 +PATCH_VER=0 +PKG_NAME=borderpatrol +BUILD_VER=0${BUILD_NUMBER}-dev + +# binaries +CC:=clang +LUAROCKS=luarocks + +# nginx and lua modules +MODULE_PATH=${PWD} +MODULE_PKG_DIR=${MODULE_PATH}/pkg +CONTRIB_PATH=${MODULE_PATH}/contrib +NGINX_PATH=${CONTRIB_PATH}/nginx +NDK_PATH=${CONTRIB_PATH}/ngx_devel_kit +MEMC_NGINX_PATH=${CONTRIB_PATH}/memc-nginx-module +LUA_MODULE_PATH=${CONTRIB_PATH}/lua-nginx-module +ECHO_MODULE_PATH=${CONTRIB_PATH}/echo-nginx-module +STICKY_MODULE_PATH=${CONTRIB_PATH}/nginx-sticky-module +HEADERS_MORE_MODULE_PATH=${CONTRIB_PATH}/headers-more-nginx-module + +NGINX_MODULES=--add-module=${NDK_PATH} \ + --add-module=${MEMC_NGINX_PATH} \ + --add-module=${LUA_MODULE_PATH} \ + --add-module=${STICKY_MODULE_PATH} \ + --add-module=${HEADERS_MORE_MODULE_PATH} \ + --add-module=${ECHO_MODULE_PATH} # only needed for + +# build locations +BUILD_PATH=${MODULE_PATH}/build +DESTDIR=${PWD}/${PKG_NAME} + +# packaging locations +CONF_DIR=/etc/${PKG_NAME} +SBIN_DIR=/usr/sbin +LOG_DIR=/var/log/${PKG_NAME} +SHARE_DIR=/usr/share/${PKG_NAME} + +# test locations +TEST_DIR = ${MODULE_PATH}/t +TEST_RUN_DIR = ${TEST_DIR}/servroot + +UNAME:=$(shell uname -s) + +ifeq ($(UNAME), Darwin) +CFLAGS+="-I /usr/local/include -Wno-error" +LD_FLAGS+="-L /usr/local/lib -L /usr/lib -liconv" +endif + +all: build + +$(BUILD_PATH)/.install_rocks: + @$(LUAROCKS) install luajson --to=$(BUILD_PATH)/usr + @$(LUAROCKS) install luacrypto --to=$(BUILD_PATH)/usr + @touch $(BUILD_PATH)/.install_rocks + +build: submodules compile mkdirs $(BUILD_PATH)/.install_rocks + @cp ${NGINX_PATH}/objs/nginx ${BUILD_PATH}${SBIN_DIR}/${PKG_NAME} + @cp -rp ${PWD}/src/*.lua ${BUILD_PATH}${SHARE_DIR} + @cp ${PWD}/src/robots.txt ${BUILD_PATH}${SHARE_DIR} + @cp ${PWD}/src/config/nginx.conf.sample ${BUILD_PATH}${CONF_DIR}/sites-available/${PKG_NAME}.conf.sample + @cp ${PWD}/src/ssl/server.crt ${BUILD_PATH}${CONF_DIR}/ssl/server.crt + @cp ${PWD}/src/ssl/server.key ${BUILD_PATH}${CONF_DIR}/ssl/server.key + +submodules: + git submodule init + git submodule update + +mkdirs: + @mkdir -p ${BUILD_PATH}${CONF_DIR}/conf.d + @mkdir -p ${BUILD_PATH}${CONF_DIR}/sites-available + @mkdir -p ${BUILD_PATH}${CONF_DIR}/sites-enabled + @mkdir -p ${BUILD_PATH}${CONF_DIR}/ssl + @mkdir -p ${BUILD_PATH}${SHARE_DIR} + @mkdir -p ${BUILD_PATH}${CONF_DIR} + @mkdir -p ${BUILD_PATH}${SBIN_DIR} + +compile: + @if [ ! -f ${NGINX_PATH}/Makefile ]; then (cd ${NGINX_PATH} && \ + ./configure --prefix=/usr \ + --sbin-path=${SBIN_DIR}/borderpatrol \ + --conf-path=${CONF_DIR}/borderpatrol.conf \ + --pid-path=/var/run/borderpatrol.pid \ + --error-log-path=${LOG_DIR}/error.log \ + --http-log-path=${LOG_DIR}/access.log \ + ${NGINX_MODULES} \ + --with-ld-opt=${LD_FLAGS} \ + --with-cc-opt=${CFLAGS} \ + --with-http_ssl_module); \ + fi; + @(cd ${NGINX_PATH} && make -j2) + +.PHONY : test + +test: build + @TEST_NGINX_BINARY=${PKG_NAME} PATH=${BUILD_PATH}${SBIN_DIR}:${PATH} prove -r ${TEST_DIR}/*.t + +mocktest: build + god -Dbc t/borderpatrol.god + +make clean: + rm -rf ${BUILD_PATH} + rm -rf ${DESTDIR} + rm -rf ngx_borderpatrol* + rm -rf *.deb + +distclean: clean + (cd ${NGINX_PATH} && if [ -f Makefile ]; then make clean; fi;) + +pkg: test + + # copy the build target dir to the package dir + rm -rf ${DESTDIR} + mv ${BUILD_PATH} ${DESTDIR} + + # Install configs under /etc/borderpatrol + cp ${MODULE_PKG_DIR}/borderpatrol.conf ${DESTDIR}${CONF_DIR}/borderpatrol.conf + chmod 0600 ${DESTDIR}${CONF_DIR}/borderpatrol.conf + chmod 0600 ${DESTDIR}${CONF_DIR}/sites-available/* + + # Install package hooks + cp ${MODULE_PKG_DIR}/after-install.sh ${DESTDIR} + + # Setup upstart config + mkdir -p ${DESTDIR}/etc/init.d + cp ${MODULE_PKG_DIR}/borderpatrol.init ${DESTDIR}/etc/init.d/borderpatrol + chmod 755 ${DESTDIR}/etc/init.d/borderpatrol + + # Create extra directories + mkdir -p ${DESTDIR}/var/log/borderpatrol + mkdir -p ${DESTDIR}/var/borderpatrol + mkdir -p ${DESTDIR}/var/cache/borderpatrol + + ########################################################################## + # create the borderpatrol package + + # install fpm if needed + test -n "$(shell gem query --local fpm|grep fpm)" || gem install fpm + + cd ${DESTDIR} && fpm -s dir -t deb -n borderpatrol -v ${MAJOR_VER}.${MINOR_VER}.${PATCH_VER}-${BUILD_VER} -C ${DESTDIR} \ + -p borderpatrol-VERSION_ARCH.deb \ + --after-install after-install.sh \ + -d libssl1.0.0 \ + -d luarocks \ + usr/ etc/ var/ + + mv ${DESTDIR}/*.deb ${PWD}/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9475c78 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# BorderPatrol for Nginx + +BorderPatrol is an nginx module to perform authentication and session management at the border of your network. + +BorderPatrol makes the assumption that you have some set of services that require authentication and a service that +hands out tokens to clients to access that service. You may not want those tokens to be sent across the internet, even +over SSL, for a variety of reasons. To this end, BorderPatrol maintains a lookup table of session-id to auth token +in memcached. + +## Overview Diagram + + +-------------+ + | BROWSER | + +--+----------+ + | ^ + REQ | | RESP + | | + v | SVC + +------------+----+ CALL +-------------------------------+ + | +------->| SERVICE A REQUIRING | + | |<-------| AUTHENTICATION | + | | +-------------------------------+ + | NGINX | + | | +-------------------------------+ + | +------->| SERVICE B REQUIRING | + | |<-------| AUTHENTICATION | + +-----------------+ +-------------------------------+ + | ^ | ^ + CACHE | | | | AUTH + LOOKUP | | | | LOOKUP + v | v | + +-----------+-+ +---+----------+ + | SESSION | | AUTH | + | STORE | | SERVICE | + +-------------+ +--------------+ + +## Use cases + +**Assumption:** All content to be access via BorderPatrol requires authentication + +There are three primary use cases for BorderPatrol: + +* A client has an auth token in the session store and the request is forwarded to the downstream service -or- +* A client does not have an auth_token for the specified service but has a master token, a call to the auth service will be made to get a service token for the downstream service -or- +* A client does not have an auth_token, and the client is redirected to a login page which posts back to nginx, performs an auth service lookup (and returns a master token and a service token from the auth service) and, on success, creates an entry in the session store for subsequent requests. + +### Use Case 1: Authorized Access + +* Client requests a protected resource via BorderPatrol +* BorderPatrol looks up the session_id from the HTTP request in the SessionStore +* If service token present, BorderPatrol sets the Auth-Token header to the service token and allows the request to continue to the protected resource + +### Use Case 2: Unauthorized Access + +* Client requests a protected resource via BorderPatrol +* BorderPatrol looks up the session_id from the HTTP request in the SessionStore +* Record exists in cache and there is a master token but no service token for specified downstream service +* A call is made to the Auth Service using the master token to get a service token +* BorderPatrol updates the session_id/{master_token, service_token_1, service_token_2...} pair in the SessionStore with appropriate expiry +* BorderPatrol redirects with the appropriate service Auth-Token header to the protected resource + +### Use Case 3: Unauthorized Access + +* Client requests a protected resource via BorderPatrol +* BorderPatrol looks up the session_id from the HTTP request in the SessionStore +* If there is a cache miss, BorderPatrol serves up a login page +* On submittal, this posts to the AuthService (via BorderPatrol) +* On successful authentication (which returns a master token and a service token for the downstream service), the AuthService sets the Auth-Token header +* BorderPatrol sets the session_id/{master_token, service_token} pair in the SessionStore with appropriate expiry +* BorderPatrol redirects with the appropriate service Auth-Token header to the protected resource + +### Caching detail + +The tokens cached in the session store are a string representation of a JSON structure as follows. + +{ + "master_token" : "MMM", + "service_tokens" : { "service_a": "AAA", "service_b": "BBB" } +} + +The token that has the key of 'master_token' is the Master Token, and can be used to make a call to the Auth Service to get other service tokens. +Service Tokens have a key name that corresponds to the name of the downstream service. + + +### Installation + +#### Darwin + +* get homebrew (http://mxcl.github.io/homebrew/) +* brew install luarocks +* brew install pcre +* brew install lua +* brew install luajit +* make + +#### Linux +* apt-get install luarocks +* make + +### Running unit tests + +You'll need the Test::Nginx CPAN module. + +* cpan install Test::Nginx +* make test + +### Running full mock services locally + +* bundle install +* make mocktest +* In a browser, hit https://localhost:4443/b/ + +### Additional Notes + +make mocktest uses God to run 4 processes, on the following ports +4443 Mock BorderPatrol +9081 Mock Authorization service +9082 Mock downstream service A +9083 Mock downstream service B + +Once you stop Mocktest, manually kill the processes above diff --git a/config b/config new file mode 100644 index 0000000..f2c9dcf --- /dev/null +++ b/config @@ -0,0 +1 @@ +have=NDK_HTTP . auto/have diff --git a/contrib/nginx-sticky-module/LICENSE b/contrib/nginx-sticky-module/LICENSE new file mode 100644 index 0000000..0654c57 --- /dev/null +++ b/contrib/nginx-sticky-module/LICENSE @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) + * + * 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/contrib/nginx-sticky-module/README b/contrib/nginx-sticky-module/README new file mode 100644 index 0000000..77bff37 --- /dev/null +++ b/contrib/nginx-sticky-module/README @@ -0,0 +1,129 @@ +Nginx Sticky Module +-- + +Description: + A nginx module to add a sticky cookie to be always forwarded the the same + upstream server. + + When dealing with several backend servers, it's sometimes useful that one + client (browser) is always served by the same backend server + (for session persistance for example). + + Using a persistance by IP (with the ip_hash upstream module) is maybe not + a good idea because there could be situations where a lot of different + browsers are coming with the same IP address (behind proxies)and the load + balancing system won't be fair. + + Using a cookie to track the upstream server makes each browser unique. + + When the sticky module can't apply, it switchs back to the classic Round Robin + Upstream or returns a "Bad Gateway" (depending on the no_fallback flag). + + Sticky module can't apply when cookies are not supported by the browser + + * Sticky module is based on a "best effort" algorithm. Its aim is not to handle + * security somehow. It's been made to ensure that normal users are always + * redirected to the same backend server: that's all! + +Installation + + You'll need to re-compile Nginx from source to include this module. + Modify your compile of Nginx by adding the following directive + (modified to suit your path of course): + + ./configure ... --add-module=/absolute/path/to/nginx-sticky-module + make + make install + +Usage + upstream { + sticky; + server 127.0.0.1:9000; + server 127.0.0.1:9001; + server 127.0.0.1:9002; + } + + sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h] [hash=index|md5|sha1] [no_fallback]; + - name: the name of the cookies used to track the persistant upstream srv + default: route + + - domain: the domain in which the cookie will be valid + default: nothing. Let the browser handle this. + + - path: the path in which the cookie will be valid + default: nothing. Let the browser handle this. + + - expires: the validity duration of the cookie + default: nothing. It's a session cookie. + restriction: must be a duration greater than one second + + - hash: the hash mechanism to encode upstream server. It cant' be used + with hmac. + md5|sha1: well known hash + index: it's not hashed, an in-memory index is used instead + it's quicker and the overhead is shorter + Warning: the matching against upstream servers list + is inconsistent. So, at reload, if upstreams servers + has changed, index values are not guaranted to + correspond to the same server as before! + USE IT WITH CAUTION and only if you need to! + default: md5 + + - hmac: the HMAC hash mechanism to encode upstream server + It's like the hash mechanism but it uses hmac_key + to secure the hashing. It can't be used with hash. + md5|sha1: well known hash + default: none. see hash. + + -hmac_key: the key to use with hmac. It's mandatory when hmac is set + default: nothing. + + -no_fallback: when this flag is set, nginx will return a 502 (Bad Gateway or + Proxy Error) if a request comes with a cookie and the + corresponding backend is unavailable. + +Detail Mechanism + see docs/sticky.{vsd,pdf} + +Warnings: + - sticky module does not work with the "backup" option of the "server" configuration item. + - sticky module does not work with the nginx_http_upstream_check_module. + - sticky module may require to configure nginx with SSL support. + +Contributing + http://code.google.com/p/nginx-sticky-module/ + +TODO + Stress + Code review + +Author + Jerome Loyet + +Copyright & License + This module is licenced under the BSD license. + + Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) + + 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/contrib/nginx-sticky-module/config b/contrib/nginx-sticky-module/config new file mode 100644 index 0000000..bf9bacc --- /dev/null +++ b/contrib/nginx-sticky-module/config @@ -0,0 +1,6 @@ +ngx_addon_name=ngx_http_sticky_module +HTTP_MODULES="$HTTP_MODULES ngx_http_sticky_module" +NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_sticky_module.c $ngx_addon_dir/ngx_http_sticky_misc.c" +NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/ngx_http_sticky_misc.h" +USE_MD5=YES +USE_SHA1=YES diff --git a/contrib/nginx-sticky-module/docs/sticky.pdf b/contrib/nginx-sticky-module/docs/sticky.pdf new file mode 100644 index 0000000..ae74c1d Binary files /dev/null and b/contrib/nginx-sticky-module/docs/sticky.pdf differ diff --git a/contrib/nginx-sticky-module/docs/sticky.vsd b/contrib/nginx-sticky-module/docs/sticky.vsd new file mode 100644 index 0000000..afbe512 Binary files /dev/null and b/contrib/nginx-sticky-module/docs/sticky.vsd differ diff --git a/contrib/nginx-sticky-module/ngx_http_sticky_misc.c b/contrib/nginx-sticky-module/ngx_http_sticky_misc.c new file mode 100644 index 0000000..237ce5d --- /dev/null +++ b/contrib/nginx-sticky-module/ngx_http_sticky_misc.c @@ -0,0 +1,315 @@ + +/* + * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) + */ + +#include +#include +#include +#include +#include +#include + +#include "ngx_http_sticky_misc.h" + +#ifndef ngx_str_set + #define ngx_str_set(str, text) (str)->len = sizeof(text) - 1; (str)->data = (u_char *) text +#endif + +ngx_int_t ngx_http_sticky_misc_set_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires) +{ + u_char *cookie, *p; + size_t len; + ngx_table_elt_t *set_cookie, *elt; + ngx_str_t remove; + ngx_list_part_t *part; + ngx_uint_t i; + + if (value == NULL) { + ngx_str_set(&remove, "_remove_"); + value = &remove; + } + + /* name = value */ + len = name->len + 1 + value->len; + + /*; Domain= */ + if (domain->len > 0) { + len += sizeof("; Domain=") - 1 + domain->len; + } + + /*; Max-Age= */ + if (expires != NGX_CONF_UNSET) { + len += sizeof("; Max-Age=") - 1 + NGX_TIME_T_LEN; + } + + /* ; Path= */ + if (path->len > 0) { + len += sizeof("; Path=") - 1 + path->len; + } + + cookie = ngx_pnalloc(r->pool, len); + if (cookie == NULL) { + return NGX_ERROR; + } + + p = ngx_copy(cookie, name->data, name->len); + *p++ = '='; + p = ngx_copy(p, value->data, value->len); + + if (domain->len > 0) { + p = ngx_copy(p, "; Domain=", sizeof("; Domain=") - 1); + p = ngx_copy(p, domain->data, domain->len); + } + + if (expires != NGX_CONF_UNSET) { + p = ngx_copy(p, "; Max-Age=", sizeof("; Max-Age=") - 1); + p = ngx_snprintf(p, NGX_TIME_T_LEN, "%T", expires); + } + + if (path->len > 0) { + p = ngx_copy(p, "; Path=", sizeof("; Path=") - 1); + p = ngx_copy(p, path->data, path->len); + } + + part = &r->headers_out.headers.part; + elt = part->elts; + set_cookie = NULL; + + for (i=0 ;; i++) { + if (part->nelts > 1 || i >= part->nelts) { + if (part->next == NULL) { + break; + } + part = part->next; + elt = part->elts; + i = 0; + } + /* ... */ + if (ngx_strncmp(elt->value.data, name->data, name->len) == 0) { + set_cookie = elt; + break; + } + } + + /* found a Set-Cookie header with the same name: replace it */ + if (set_cookie != NULL) { + set_cookie->value.len = p - cookie; + set_cookie->value.data = cookie; + return NGX_OK; + } + + set_cookie = ngx_list_push(&r->headers_out.headers); + if (set_cookie == NULL) { + return NGX_ERROR; + } + set_cookie->hash = 1; + ngx_str_set(&set_cookie->key, "Set-Cookie"); + set_cookie->value.len = p - cookie; + set_cookie->value.data = cookie; + + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest) +{ + ngx_md5_t md5; + u_char hash[MD5_DIGEST_LENGTH]; + + digest->data = ngx_pcalloc(pool, MD5_DIGEST_LENGTH * 2); + if (digest->data == NULL) { + return NGX_ERROR; + } + + digest->len = MD5_DIGEST_LENGTH * 2; + ngx_md5_init(&md5); + ngx_md5_update(&md5, in, len); + ngx_md5_final(hash, &md5); + + ngx_hex_dump(digest->data, hash, MD5_DIGEST_LENGTH); + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest) +{ + ngx_sha1_t sha1; + u_char hash[SHA_DIGEST_LENGTH]; + + digest->data = ngx_pcalloc(pool, SHA_DIGEST_LENGTH * 2); + if (digest->data == NULL) { + return NGX_ERROR; + } + + digest->len = SHA_DIGEST_LENGTH * 2; + ngx_sha1_init(&sha1); + ngx_sha1_update(&sha1, in, len); + ngx_sha1_final(hash, &sha1); + + ngx_hex_dump(digest->data, hash, SHA_DIGEST_LENGTH); + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_hmac_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest) +{ + u_char hash[MD5_DIGEST_LENGTH]; + u_char k[MD5_CBLOCK]; + ngx_md5_t md5; + u_int i; + + digest->data = ngx_pcalloc(pool, MD5_DIGEST_LENGTH * 2); + if (digest->data == NULL) { + return NGX_ERROR; + } + digest->len = MD5_DIGEST_LENGTH * 2; + + ngx_memzero(k, sizeof(k)); + + if (key->len > MD5_CBLOCK) { + ngx_md5_init(&md5); + ngx_md5_update(&md5, key->data, key->len); + ngx_md5_final(k, &md5); + } else { + ngx_memcpy(k, key->data, key->len); + } + + /* XOR ipad */ + for (i=0; i < MD5_CBLOCK; i++) { + k[i] ^= 0x36; + } + + ngx_md5_init(&md5); + ngx_md5_update(&md5, k, MD5_CBLOCK); + ngx_md5_update(&md5, in, len); + ngx_md5_final(hash, &md5); + + /* Convert k to opad -- 0x6A = 0x36 ^ 0x5C */ + for (i=0; i < MD5_CBLOCK; i++) { + k[i] ^= 0x6a; + } + + ngx_md5_init(&md5); + ngx_md5_update(&md5, k, MD5_CBLOCK); + ngx_md5_update(&md5, hash, MD5_DIGEST_LENGTH); + ngx_md5_final(hash, &md5); + + ngx_hex_dump(digest->data, hash, MD5_DIGEST_LENGTH); + + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_hmac_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest) +{ + u_char hash[SHA_DIGEST_LENGTH]; + u_char k[SHA_CBLOCK]; + ngx_sha1_t sha1; + u_int i; + + digest->data = ngx_pcalloc(pool, SHA_DIGEST_LENGTH * 2); + if (digest->data == NULL) { + return NGX_ERROR; + } + digest->len = SHA_DIGEST_LENGTH * 2; + + ngx_memzero(k, sizeof(k)); + + if (key->len > SHA_CBLOCK) { + ngx_sha1_init(&sha1); + ngx_sha1_update(&sha1, key->data, key->len); + ngx_sha1_final(k, &sha1); + } else { + ngx_memcpy(k, key->data, key->len); + } + + /* XOR ipad */ + for (i=0; i < SHA_CBLOCK; i++) { + k[i] ^= 0x36; + } + + ngx_sha1_init(&sha1); + ngx_sha1_update(&sha1, k, SHA_CBLOCK); + ngx_sha1_update(&sha1, in, len); + ngx_sha1_final(hash, &sha1); + + /* Convert k to opad -- 0x6A = 0x36 ^ 0x5C */ + for (i=0; i < SHA_CBLOCK; i++) { + k[i] ^= 0x6a; + } + + ngx_sha1_init(&sha1); + ngx_sha1_update(&sha1, k, SHA_CBLOCK); + ngx_sha1_update(&sha1, hash, SHA_DIGEST_LENGTH); + ngx_sha1_final(hash, &sha1); + + ngx_hex_dump(digest->data, hash, SHA_DIGEST_LENGTH); + + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_text_raw(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) +{ + size_t len; + if (!in) { + return NGX_ERROR; + } + + switch (in->sa_family) { + case AF_INET: + len = NGX_INET_ADDRSTRLEN + sizeof(":65535") - 1; + break; + +#if (NGX_HAVE_INET6) + case AF_INET6: + len = NGX_INET6_ADDRSTRLEN + sizeof(":65535") - 1; + break; +#endif + +#if (NGX_HAVE_UNIX_DOMAIN) + case AF_UNIX: + len = sizeof("unix:") - 1 + NGX_UNIX_ADDRSTRLEN; + break; +#endif + + default: + return NGX_ERROR; + } + + + digest->data = ngx_pnalloc(pool, len); + if (digest->data == NULL) { + return NGX_ERROR; + } + digest->len = ngx_sock_ntop(in, digest->data, len, 1); + return NGX_OK; + return NGX_OK; +} + +ngx_int_t ngx_http_sticky_misc_text_md5(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) +{ + ngx_str_t str; + if (ngx_http_sticky_misc_text_raw(pool, in, &str) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_http_sticky_misc_md5(pool, (void *)str.data, str.len, digest) != NGX_OK) { + ngx_pfree(pool, &str); + return NGX_ERROR; + } + + return ngx_pfree(pool, &str); +} + +ngx_int_t ngx_http_sticky_misc_text_sha1(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest) +{ + ngx_str_t str; + if (ngx_http_sticky_misc_text_raw(pool, in, &str) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_http_sticky_misc_sha1(pool, (void *)str.data, str.len, digest) != NGX_OK) { + ngx_pfree(pool, &str); + return NGX_ERROR; + } + + return ngx_pfree(pool, &str); +} + diff --git a/contrib/nginx-sticky-module/ngx_http_sticky_misc.h b/contrib/nginx-sticky-module/ngx_http_sticky_misc.h new file mode 100644 index 0000000..fe8e859 --- /dev/null +++ b/contrib/nginx-sticky-module/ngx_http_sticky_misc.h @@ -0,0 +1,28 @@ + +/* + * Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net) + */ + +#ifndef _NGX_HTTP_STICKY_MISC_H_INCLUDED_ +#define _NGX_HTTP_STICKY_MISC_H_INCLUDED_ + +#include +#include +#include +#include + +typedef ngx_int_t (*ngx_http_sticky_misc_hash_pt)(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); +typedef ngx_int_t (*ngx_http_sticky_misc_hmac_pt)(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); +typedef ngx_int_t (*ngx_http_sticky_misc_text_pt)(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); + +ngx_int_t ngx_http_sticky_misc_set_cookie (ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value, ngx_str_t *domain, ngx_str_t *path, time_t expires); +ngx_int_t ngx_http_sticky_misc_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); +ngx_int_t ngx_http_sticky_misc_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *digest); +ngx_int_t ngx_http_sticky_misc_hmac_md5(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); +ngx_int_t ngx_http_sticky_misc_hmac_sha1(ngx_pool_t *pool, void *in, size_t len, ngx_str_t *key, ngx_str_t *digest); + +ngx_int_t ngx_http_sticky_misc_text_raw(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); +ngx_int_t ngx_http_sticky_misc_text_md5(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); +ngx_int_t ngx_http_sticky_misc_text_sha1(ngx_pool_t *pool, struct sockaddr *in, ngx_str_t *digest); + +#endif /* _NGX_HTTP_STICKY_MISC_H_INCLUDED_ */ diff --git a/contrib/nginx-sticky-module/ngx_http_sticky_module.c b/contrib/nginx-sticky-module/ngx_http_sticky_module.c new file mode 100644 index 0000000..82d8f5f --- /dev/null +++ b/contrib/nginx-sticky-module/ngx_http_sticky_module.c @@ -0,0 +1,691 @@ + +/* + * Copyright (C) Jerome Loyet + */ + + +#include +#include +#include + +#include "ngx_http_sticky_misc.h" + +/* define a peer */ +typedef struct { + ngx_http_upstream_rr_peer_t *rr_peer; + ngx_str_t digest; +} ngx_http_sticky_peer_t; + +/* the configuration structure */ +typedef struct { + ngx_http_upstream_srv_conf_t uscf; + ngx_str_t cookie_name; + ngx_str_t cookie_domain; + ngx_str_t cookie_path; + time_t cookie_expires; + ngx_str_t hmac_key; + ngx_http_sticky_misc_hash_pt hash; + ngx_http_sticky_misc_hmac_pt hmac; + ngx_http_sticky_misc_text_pt text; + ngx_uint_t no_fallback; + ngx_http_sticky_peer_t *peers; +} ngx_http_sticky_srv_conf_t; + + +/* the custom sticky struct used on each request */ +typedef struct { + /* the round robin data must be first */ + ngx_http_upstream_rr_peer_data_t rrp; + ngx_event_get_peer_pt get_rr_peer; + int selected_peer; + int no_fallback; + ngx_http_sticky_srv_conf_t *sticky_conf; + ngx_http_request_t *request; +} ngx_http_sticky_peer_data_t; + + +static ngx_int_t ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us); +static ngx_int_t ngx_http_get_sticky_peer(ngx_peer_connection_t *pc, void *data); +static char *ngx_http_sticky_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static void *ngx_http_sticky_create_conf(ngx_conf_t *cf); + + +static ngx_command_t ngx_http_sticky_commands[] = { + + { ngx_string("sticky"), + NGX_HTTP_UPS_CONF|NGX_CONF_ANY, + ngx_http_sticky_set, + 0, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_sticky_module_ctx = { + NULL, /* preconfiguration */ + NULL, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + ngx_http_sticky_create_conf, /* create server configuration */ + NULL, /* merge server configuration */ + + NULL, /* create location configuration */ + NULL /* merge location configuration */ +}; + + +ngx_module_t ngx_http_sticky_module = { + NGX_MODULE_V1, + &ngx_http_sticky_module_ctx, /* module context */ + ngx_http_sticky_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 +}; + + +/* + * function called by the upstream module to init itself + * it's called once per instance + */ +ngx_int_t ngx_http_init_upstream_sticky(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) +{ + ngx_http_upstream_rr_peers_t *rr_peers; + ngx_http_sticky_srv_conf_t *conf; + ngx_uint_t i; + + /* call the rr module on wich the sticky module is based on */ + if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) { + return NGX_ERROR; + } + + /* calculate each peer digest once and save */ + rr_peers = us->peer.data; + + /* do nothing there's only one peer */ + if (rr_peers->number <= 1 || rr_peers->single) { + return NGX_OK; + } + + /* tell the upstream module to call ngx_http_init_sticky_peer when it inits peer */ + us->peer.init = ngx_http_init_sticky_peer; + + conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module); + + /* if 'index', no need to alloc and generate digest */ + if (!conf->hash && !conf->hmac && !conf->text) { + conf->peers = NULL; + return NGX_OK; + } + + /* create our own upstream indexes */ + conf->peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_peer_t) * rr_peers->number); + if (conf->peers == NULL) { + return NGX_ERROR; + } + + /* parse each peer and generate digest if necessary */ + for (i = 0; i < rr_peers->number; i++) { + conf->peers[i].rr_peer = &rr_peers->peer[i]; + + if (conf->hmac) { + /* generate hmac */ + conf->hmac(cf->pool, rr_peers->peer[i].sockaddr, rr_peers->peer[i].socklen, &conf->hmac_key, &conf->peers[i].digest); + + } else if (conf->text) { + /* generate text */ + conf->text(cf->pool, rr_peers->peer[i].sockaddr, &conf->peers[i].digest); + + } else { + /* generate hash */ + conf->hash(cf->pool, rr_peers->peer[i].sockaddr, rr_peers->peer[i].socklen, &conf->peers[i].digest); + } + +#if 0 +/* FIXME: is it possible to log to debug level when at configuration stage ? */ + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "[sticky/ngx_http_init_upstream_sticky] generated digest \"%V\" for upstream at index %d", &conf->peers[i].digest, i); +#endif + + } + + return NGX_OK; +} + +/* + * function called by the upstream module when it inits each peer + * it's called once per request + */ +static ngx_int_t ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us) +{ + ngx_http_sticky_peer_data_t *iphp; + ngx_str_t route; + ngx_uint_t i; + ngx_int_t n; + + /* alloc custom sticky struct */ + iphp = ngx_palloc(r->pool, sizeof(ngx_http_sticky_peer_data_t)); + if (iphp == NULL) { + return NGX_ERROR; + } + + /* attach it to the request upstream data */ + r->upstream->peer.data = &iphp->rrp; + + /* call the rr module on which the sticky is based on */ + if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) { + return NGX_ERROR; + } + + /* set the callback to select the next peer to use */ + r->upstream->peer.get = ngx_http_get_sticky_peer; + + /* init the custom sticky struct */ + iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; + iphp->selected_peer = -1; + iphp->no_fallback = 0; + iphp->sticky_conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module); + iphp->request = r; + + /* check weather a cookie is present or not and save it */ + if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies, &iphp->sticky_conf->cookie_name, &route) != NGX_DECLINED) { + /* a route cookie has been found. Let's give it a try */ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] got cookie route=%V, let's try to find a matching peer", &route); + + /* hash, hmac or text, just compare digest */ + if (iphp->sticky_conf->hash || iphp->sticky_conf->hmac || iphp->sticky_conf->text) { + + /* check internal struct has been set */ + if (!iphp->sticky_conf->peers) { + /* log a warning, as it will continue without the sticky */ + ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "[sticky/init_sticky_peer] internal peers struct has not been set"); + return NGX_OK; /* return OK, in order to continue */ + } + + /* search the digest found in the cookie in the peer digest list */ + for (i = 0; i < iphp->rrp.peers->number; i++) { + + /* ensure the both len are equal and > 0 */ + if (iphp->sticky_conf->peers[i].digest.len != route.len || route.len <= 0) { + continue; + } + + if (!ngx_strncmp(iphp->sticky_conf->peers[i].digest.data, route.data, route.len)) { + /* we found a match */ + iphp->selected_peer = i; + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" matches peer at index %ui", &route, i); + return NGX_OK; + } + } + + } else { + + /* switch back to index, just convert to integer and ensure it corresponds to a valid peer */ + n = ngx_atoi(route.data, route.len); + if (n == NGX_ERROR) { + ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "[sticky/init_sticky_peer] unable to convert the route \"%V\" to an integer value", &route); + } else if (n >= 0 && n < (ngx_int_t)iphp->rrp.peers->number) { + /* found one */ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" matches peer at index %i", &route, n); + iphp->selected_peer = n; + return NGX_OK; + } + } + + /* nothing was found, just continue with rr */ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] the route \"%V\" does not match any peer. Just ignoring it ...", &route); + return NGX_OK; + } + + /* nothing found */ + ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "[sticky/init_sticky_peer] route cookie not found", &route); + return NGX_OK; /* return OK, in order to continue */ +} + +/* + * function called by the upstream module to choose the next peer to use + * called at least one time per request + */ +static ngx_int_t ngx_http_get_sticky_peer(ngx_peer_connection_t *pc, void *data) +{ + ngx_http_sticky_peer_data_t *iphp = data; + ngx_http_sticky_srv_conf_t *conf = iphp->sticky_conf; + ngx_int_t selected_peer = -1; + time_t now = ngx_time(); + uintptr_t m; + ngx_uint_t n, i; + ngx_http_upstream_rr_peer_t *peer = NULL; + + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] get sticky peer, try: %ui, n_peers: %ui, no_fallback: %ui/%ui", pc->tries, iphp->rrp.peers->number, conf->no_fallback, iphp->no_fallback); + + /* TODO: cached */ + + /* has the sticky module already choosen a peer to connect to and is it a valid peer */ + /* is there more than one peer (otherwise, no choices to make) */ + if (iphp->selected_peer >= 0 && iphp->selected_peer < (ngx_int_t)iphp->rrp.peers->number && !iphp->rrp.peers->single) { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] let's try the selected peer (%i)", iphp->selected_peer); + + n = iphp->selected_peer / (8 * sizeof(uintptr_t)); + m = (uintptr_t) 1 << iphp->selected_peer % (8 * sizeof(uintptr_t)); + + /* has the peer not already been tried ? */ + if (!(iphp->rrp.tried[n] & m)) { + peer = &iphp->rrp.peers->peer[iphp->selected_peer]; + + /* if the no_fallback flag is set */ + if (conf->no_fallback) { + + iphp->no_fallback = 1; + + /* if peer is down */ + if (peer->down) { + ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] the selected peer is down and no_fallback is flagged"); + return NGX_BUSY; + } + + /* if it's been ignored for long enought (fail_timeout), reset timeout */ + /* do this check before testing peer->fails ! :) */ + if (now - peer->accessed > peer->fail_timeout) { + peer->fails = 0; + } + + /* if peer is failed */ + if (peer->max_fails > 0 && peer->fails >= peer->max_fails) { + ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] the selected peer is maked as failed and no_fallback is flagged"); + return NGX_BUSY; + } + } + + /* ensure the peer is not marked as down */ + if (!peer->down) { + + /* if it's not failedi, use it */ + if (peer->max_fails == 0 || peer->fails < peer->max_fails) { + selected_peer = (ngx_int_t)n; + + /* if it's been ignored for long enought (fail_timeout), reset timeout and use it */ + } else if (now - peer->accessed > peer->fail_timeout) { + peer->fails = 0; + selected_peer = (ngx_int_t)n; + + /* it's failed or timeout did not expire yet */ + } else { + /* mark the peer as tried */ + iphp->rrp.tried[n] |= m; + } + } + } + } + + /* we have a valid peer, tell the upstream module to use it */ + if (peer && selected_peer >= 0) { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] peer found at index %i", selected_peer); + + iphp->rrp.current = iphp->selected_peer; + pc->cached = 0; + pc->connection = NULL; + pc->sockaddr = peer->sockaddr; + pc->socklen = peer->socklen; + pc->name = &peer->name; + + iphp->rrp.tried[n] |= m; + + } else { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] no sticky peer selected, switch back to classic rr"); + + if (iphp->no_fallback) { + ngx_log_error(NGX_LOG_NOTICE, pc->log, 0, "[sticky/get_sticky_peer] No fallback in action !"); + return NGX_BUSY; + } + + ngx_int_t ret = iphp->get_rr_peer(pc, &iphp->rrp); + if (ret != NGX_OK) { + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] ngx_http_upstream_get_round_robin_peer returned %i", ret); + return ret; + } + + /* search for the choosen peer in order to set the cookie */ + for (i = 0; i < iphp->rrp.peers->number; i++) { + + if (iphp->rrp.peers->peer[i].sockaddr == pc->sockaddr && iphp->rrp.peers->peer[i].socklen == pc->socklen) { + if (conf->hash || conf->hmac || conf->text) { + ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &conf->peers[i].digest, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires); + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &conf->peers[i].digest, i); + } else { + ngx_str_t route; + ngx_uint_t tmp = i; + route.len = 0; + do { + route.len++; + } while (tmp /= 10); + route.data = ngx_pcalloc(iphp->request->pool, sizeof(u_char) * (route.len + 1)); + if (route.data == NULL) { + break; + } + ngx_snprintf(route.data, route.len, "%d", i); + route.len = ngx_strlen(route.data); + ngx_http_sticky_misc_set_cookie(iphp->request, &conf->cookie_name, &route, &conf->cookie_domain, &conf->cookie_path, conf->cookie_expires); + ngx_log_debug(NGX_LOG_DEBUG_HTTP, pc->log, 0, "[sticky/get_sticky_peer] set cookie \"%V\" value=\"%V\" index=%ui", &conf->cookie_name, &tmp, i); + } + break; /* found and hopefully the cookie have been set */ + } + } + } + + /* reset the selection in order to bypass the sticky module when the upstream module will try another peers if necessary */ + iphp->selected_peer = -1; + + return NGX_OK; +} + +/* + * Function called when the sticky command is parsed on the conf file + */ +static char *ngx_http_sticky_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_upstream_srv_conf_t *upstream_conf; + ngx_http_sticky_srv_conf_t *sticky_conf; + ngx_uint_t i; + ngx_str_t tmp; + ngx_str_t name = ngx_string("route"); + ngx_str_t domain = ngx_string(""); + ngx_str_t path = ngx_string(""); + ngx_str_t hmac_key = ngx_string(""); + time_t expires = NGX_CONF_UNSET; + ngx_http_sticky_misc_hash_pt hash = NGX_CONF_UNSET_PTR; + ngx_http_sticky_misc_hmac_pt hmac = NULL; + ngx_http_sticky_misc_text_pt text = NULL; + ngx_uint_t no_fallback = 0; + + /* parse all elements */ + for (i = 1; i < cf->args->nelts; i++) { + ngx_str_t *value = cf->args->elts; + + /* is "name=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "name=") == value[i].data) { + + /* do we have at least on char after "name=" ? */ + if (value[i].len <= sizeof("name=") - 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"name=\""); + return NGX_CONF_ERROR; + } + + /* save what's after "name=" */ + name.len = value[i].len - ngx_strlen("name="); + name.data = (u_char *)(value[i].data + sizeof("name=") - 1); + continue; + } + + /* is "domain=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "domain=") == value[i].data) { + + /* do we have at least on char after "domain=" ? */ + if (value[i].len <= ngx_strlen("domain=")) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"domain=\""); + return NGX_CONF_ERROR; + } + + /* save what's after "domain=" */ + domain.len = value[i].len - ngx_strlen("domain="); + domain.data = (u_char *)(value[i].data + sizeof("domain=") - 1); + continue; + } + + /* is "path=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "path=") == value[i].data) { + + /* do we have at least on char after "path=" ? */ + if (value[i].len <= ngx_strlen("path=")) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"path=\""); + return NGX_CONF_ERROR; + } + + /* save what's after "domain=" */ + path.len = value[i].len - ngx_strlen("path="); + path.data = (u_char *)(value[i].data + sizeof("path=") - 1); + continue; + } + + /* is "expires=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "expires=") == value[i].data) { + + /* do we have at least on char after "expires=" ? */ + if (value[i].len <= sizeof("expires=") - 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"expires=\""); + return NGX_CONF_ERROR; + } + + /* extract value */ + tmp.len = value[i].len - ngx_strlen("expires="); + tmp.data = (u_char *)(value[i].data + sizeof("expires=") - 1); + + /* convert to time, save and validate */ + expires = ngx_parse_time(&tmp, 1); + if (expires == NGX_ERROR || expires < 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value for \"expires=\""); + return NGX_CONF_ERROR; + } + continue; + } + + /* is "text=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "text=") == value[i].data) { + + /* only hash or hmac can be used, not both */ + if (hmac || hash != NGX_CONF_UNSET_PTR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text\""); + return NGX_CONF_ERROR; + } + + /* do we have at least on char after "name=" ? */ + if (value[i].len <= sizeof("text=") - 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"text=\""); + return NGX_CONF_ERROR; + } + + /* extract value to temp */ + tmp.len = value[i].len - ngx_strlen("text="); + tmp.data = (u_char *)(value[i].data + sizeof("text=") - 1); + + /* is name=raw */ + if (ngx_strncmp(tmp.data, "raw", sizeof("raw") - 1) == 0 ) { + text = ngx_http_sticky_misc_text_raw; + continue; + } + + /* is name=md5 */ + if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { + text = ngx_http_sticky_misc_text_md5; + continue; + } + + /* is name=sha1 */ + if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { + text = ngx_http_sticky_misc_text_sha1; + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"text=\": raw, md5 or sha1"); + return NGX_CONF_ERROR; + } + + /* is "hash=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "hash=") == value[i].data) { + + /* only hash or hmac can be used, not both */ + if (hmac || text) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text=\""); + return NGX_CONF_ERROR; + } + + /* do we have at least on char after "hash=" ? */ + if (value[i].len <= sizeof("hash=") - 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hash=\""); + return NGX_CONF_ERROR; + } + + /* extract value to temp */ + tmp.len = value[i].len - ngx_strlen("hash="); + tmp.data = (u_char *)(value[i].data + sizeof("hash=") - 1); + + /* is hash=index */ + if (ngx_strncmp(tmp.data, "index", sizeof("index") - 1) == 0 ) { + hash = NULL; + continue; + } + + /* is hash=md5 */ + if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { + hash = ngx_http_sticky_misc_md5; + continue; + } + + /* is hash=sha1 */ + if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { + hash = ngx_http_sticky_misc_sha1; + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"hash=\": index, md5 or sha1"); + return NGX_CONF_ERROR; + } + + /* is "hmac=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "hmac=") == value[i].data) { + + /* only hash or hmac can be used, not both */ + if (hash != NGX_CONF_UNSET_PTR || text) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please choose between \"hash=\", \"hmac=\" and \"text\""); + return NGX_CONF_ERROR; + } + + /* do we have at least on char after "hmac=" ? */ + if (value[i].len <= sizeof("hmac=") - 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hmac=\""); + return NGX_CONF_ERROR; + } + + /* extract value */ + tmp.len = value[i].len - ngx_strlen("hmac="); + tmp.data = (u_char *)(value[i].data + sizeof("hmac=") - 1); + + /* is hmac=md5 ? */ + if (ngx_strncmp(tmp.data, "md5", sizeof("md5") - 1) == 0 ) { + hmac = ngx_http_sticky_misc_hmac_md5; + continue; + } + + /* is hmac=sha1 ? */ + if (ngx_strncmp(tmp.data, "sha1", sizeof("sha1") - 1) == 0 ) { + hmac = ngx_http_sticky_misc_hmac_sha1; + continue; + } + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "wrong value for \"hmac=\": md5 or sha1"); + return NGX_CONF_ERROR; + } + + /* is "hmac_key=" is starting the argument ? */ + if ((u_char *)ngx_strstr(value[i].data, "hmac_key=") == value[i].data) { + + /* do we have at least on char after "hmac_key=" ? */ + if (value[i].len <= ngx_strlen("hmac_key=")) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "a value must be provided to \"hmac_key=\""); + return NGX_CONF_ERROR; + } + + /* save what's after "hmac_key=" */ + hmac_key.len = value[i].len - ngx_strlen("hmac_key="); + hmac_key.data = (u_char *)(value[i].data + sizeof("hmac_key=") - 1); + continue; + } + + /* is "no_fallback" flag present ? */ + if (ngx_strncmp(value[i].data, "no_fallback", sizeof("no_fallback") - 1) == 0 ) { + no_fallback = 1; + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid arguement (%V)", &value[i]); + return NGX_CONF_ERROR; + } + + /* if has and hmac and name have not been set, default to md5 */ + if (hash == NGX_CONF_UNSET_PTR && hmac == NULL && text == NULL) { + hash = ngx_http_sticky_misc_md5; + } + + /* don't allow meaning less parameters */ + if (hmac_key.len > 0 && hash != NGX_CONF_UNSET_PTR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "\"hmac_key=\" is meaningless when \"hmac\" is used. Please remove it."); + return NGX_CONF_ERROR; + } + + /* ensure we have an hmac key if hmac's been set */ + if (hmac_key.len == 0 && hmac != NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "please specify \"hmac_key=\" when using \"hmac\""); + return NGX_CONF_ERROR; + } + + /* ensure hash is NULL to avoid conflicts later */ + if (hash == NGX_CONF_UNSET_PTR) { + hash = NULL; + } + + /* save the sticky parameters */ + sticky_conf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_sticky_module); + sticky_conf->cookie_name = name; + sticky_conf->cookie_domain = domain; + sticky_conf->cookie_path = path; + sticky_conf->cookie_expires = expires; + sticky_conf->hash = hash; + sticky_conf->hmac = hmac; + sticky_conf->text = text; + sticky_conf->hmac_key = hmac_key; + sticky_conf->no_fallback = no_fallback; + sticky_conf->peers = NULL; /* ensure it's null before running */ + + upstream_conf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module); + + /* + * ensure another upstream module has not been already loaded + * peer.init_upstream is set to null and the upstream module use RR if not set + * But this check only works when the other module is declared before sticky + */ + if (upstream_conf->peer.init_upstream) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "You can't use sticky with another upstream module"); + return NGX_CONF_ERROR; + } + + /* configure the upstream to get back to this module */ + upstream_conf->peer.init_upstream = ngx_http_init_upstream_sticky; + + upstream_conf->flags = NGX_HTTP_UPSTREAM_CREATE + | NGX_HTTP_UPSTREAM_MAX_FAILS + | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT + | NGX_HTTP_UPSTREAM_DOWN + | NGX_HTTP_UPSTREAM_WEIGHT; + + return NGX_CONF_OK; +} + +/* + * alloc stick configuration + */ +static void *ngx_http_sticky_create_conf(ngx_conf_t *cf) +{ + ngx_http_sticky_srv_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_srv_conf_t)); + if (conf == NULL) { + return NGX_CONF_ERROR; + } + + return conf; +} diff --git a/pkg/after-install.sh b/pkg/after-install.sh new file mode 100644 index 0000000..85051e0 --- /dev/null +++ b/pkg/after-install.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ "$(ls -A /etc/borderpatrol/sites-enabled)" ]; then + echo "/etc/borderpatrol/sites-enabled is not empty; skipping symlinking of default conf file" +else + ln -s /etc/borderpatrol/sites-available/default.conf /etc/borderpatrol/sites-enabled/default.conf || true +fi diff --git a/pkg/borderpatrol.conf b/pkg/borderpatrol.conf new file mode 100644 index 0000000..8efcd07 --- /dev/null +++ b/pkg/borderpatrol.conf @@ -0,0 +1,9 @@ +pid /var/run/borderpatrol.pid; +daemon on; + +events { + worker_connections 40; +} + +include /etc/borderpatrol/conf.d/*.conf; +include /etc/borderpatrol/sites-enabled/*.conf; diff --git a/pkg/borderpatrol.init b/pkg/borderpatrol.init new file mode 100644 index 0000000..007fbcf --- /dev/null +++ b/pkg/borderpatrol.init @@ -0,0 +1,148 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: borderpatrol +# Required-Start: $network $remote_fs $local_fs +# Required-Stop: $network $remote_fs $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Stop/start borderpatrol +### END INIT INFO + +# Author: Sergey Budnevitch + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC=borderpatrol +NAME=borderpatrol +CONFFILE=/etc/borderpatrol/borderpatrol.conf +DAEMON=/usr/sbin/borderpatrol +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +[ -x $DAEMON ] || exit 0 + +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +DAEMON_ARGS="-c $CONFFILE $DAEMON_ARGS" + +. /lib/init/vars.sh + +. /lib/lsb/init-functions + +do_start() +{ + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS + RETVAL="$?" + return "$RETVAL" +} + +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + rm -f $PIDFILE + return "$RETVAL" +} + +do_reload() { + # + start-stop-daemon --stop --signal HUP --quiet --pidfile $PIDFILE --name $NAME + RETVAL="$?" + return "$RETVAL" +} + +do_configtest() { + if [ "$#" -ne 0 ]; then + case "$1" in + -q) + FLAG=$1 + ;; + *) + ;; + esac + shift + fi + $DAEMON -t $FLAG -c $CONFFILE + RETVAL="$?" + return $RETVAL +} + +do_upgrade() { + OLDBINPIDFILE=$PIDFILE.oldbin + + do_configtest -q || return 6 + start-stop-daemon --stop --signal USR2 --quiet --pidfile $PIDFILE --name $NAME + RETVAL="$?" + sleep 1 + if [ -f $OLDBINPIDFILE -a -f $PIDFILE ]; then + start-stop-daemon --stop --signal QUIT --quiet --pidfile $OLDBINPIDFILE --name $NAME + RETVAL="$?" + else + echo $"Upgrade failed!" + RETVAL=1 + return $RETVAL + fi +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + configtest) + do_configtest + ;; + upgrade) + do_upgrade + ;; + reload|force-reload) + log_daemon_msg "Reloading $DESC" "$NAME" + do_reload + log_end_msg $? + ;; + restart|force-reload) + log_daemon_msg "Restarting $DESC" "$NAME" + do_configtest -q || exit $RETVAL + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload|upgrade|configtest}" >&2 + exit 3 + ;; +esac + +exit $RETVAL diff --git a/src/access.lua b/src/access.lua new file mode 100644 index 0000000..fe823cd --- /dev/null +++ b/src/access.lua @@ -0,0 +1,16 @@ +if (ngx.var.auth_token == "") then + -- make a sub request to pull session data + local res = ngx.location.capture("/auth") + + ngx.log(ngx.DEBUG, "==== GET /auth " .. res.status .. " " .. res.body) + + if res.status == ngx.HTTP_OK then + -- set the auth token in the request variables so it can be pulled out + -- and passed as a header then return and allow the request chain to + -- continue. if there's no auth token do the same thing, allowing the + -- upstream service to allow or deny access. + ngx.var.auth_token = res.header['Auth-Token'] + end +else + ngx.log(ngx.DEBUG, "==== skipping GET /auth since auth_token is present: [" .. ngx.var.auth_token .. "]") +end diff --git a/src/authorize.lua b/src/authorize.lua new file mode 100644 index 0000000..68e6290 --- /dev/null +++ b/src/authorize.lua @@ -0,0 +1,91 @@ +local json = require("json") +local sessionid = require("sessionid") + +------------------------------------------- +-- Make the call to the Account Service +------------------------------------------- + +local session_id = ngx.var.cookie_border_session + +-- require session because the only valid scenario for arriving here is via redirect, which should have already set +-- session +if not session_id then + ngx.log(ngx.INFO, "==== access denied: no session_id") + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end +ngx.log(ngx.DEBUG, "==== session_id: " .. session_id) + +if not sessionid.is_valid(session_id) then + ngx.log(ngx.INFO, "==== access denied: session id invalid " .. session_id) + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +-- Retrieve original target url and derive service +local res = ngx.location.capture('/session?id=BP_URL_SID_' .. session_id) + +ngx.log(ngx.DEBUG, "==== GET /session?id=BP_URL_SID_" .. session_id .. " " .. res.status) + +-- Get original downstream url they were going to before being redirected +original_url = res.body + +-- get service name from first part of original uri +local service_uri = string.match(original_url, "^/([^/]+)") +local service = nil +if service_uri then + ngx.log(ngx.DEBUG, "==== service uri is: " .. service_uri) + service = service_mappings[service_uri] +end + +-- check service +if not service then + if not service_uri then + ngx.log(ngx.DEBUG, "==== no valid service uri provided") + else + ngx.log(ngx.DEBUG, "==== service not set for service uri: " .. service_uri) + end + ngx.redirect('/account/password') +end + +ngx.req.read_body() +local args = ngx.req.get_post_args() + +-- the account service expects 'e=user@example.com&p=password&t=3&s=servicename' +args['service'] = service +res = ngx.location.capture('/account', { method = ngx.HTTP_POST, body = ngx.encode_args(args) }) + +ngx.log(ngx.DEBUG, "==== POST /account " .. res.status .. " " .. res.body) + +-- assume any 2xx is success +-- On failure, redirect to login +if res.status >= ngx.HTTP_SPECIAL_RESPONSE then + ngx.log(ngx.DEBUG, "==== Authorization against Account Service failed: " .. res.body) + ngx.redirect('/account') +end + +-- parse the response body +local all_tokens_json = res.body +local all_tokens = json.decode(all_tokens_json) + +-- looking for auth tokens +if not all_tokens then + ngx.log(ngx.DEBUG, "==== no tokens found, redirecting to /account") + ngx.redirect('/account') +end + +-- looking for service token +if not all_tokens["service_tokens"][service] then + ngx.log(ngx.DEBUG, "==== parse failure, or service token not found, redirecting to /account") + ngx.redirect('/account') +end + +-- Extract token for specific service +local auth_token = all_tokens["service_tokens"][service] + +-- store all tokens in memcache via internal subrequest +local res = ngx.location.capture('/session?id=BPSID_' .. session_id .. + '&arg_exptime=' .. sessionid.EXPTIME, { body = all_tokens_json, method = ngx.HTTP_PUT }) + +ngx.log(ngx.DEBUG, "==== PUT /session?id=BPSID_" .. session_id .. + '&arg_exptime=' .. sessionid.EXPTIME .. " " .. res.status) + +ngx.redirect(original_url) diff --git a/src/config/nginx.conf.sample b/src/config/nginx.conf.sample new file mode 100644 index 0000000..7f2fdd1 --- /dev/null +++ b/src/config/nginx.conf.sample @@ -0,0 +1,151 @@ +pid /tmp/nginx.pid; +daemon off; + +http { + lua_package_path "../../build/usr/share/borderpatrol/?.lua;../../build/usr/share/lua/5.1/?.lua;;"; + lua_package_cpath "../../build/usr/lib/lua/5.1/?.so;;"; + limit_req_zone $binary_remote_addr zone=auth_zone:100m rate=100r/m; + + error_log logs/error.log debug; + access_log logs/access.log; + + # used to store and retrieve keys from memcached + upstream session_store { + server localhost:11211; + keepalive 32; + } + + # this is an app server protected by border patrol. If it returns a 401 + # when an attempt is made to access a protected resource, borderpatrol redirects + # to the account service login + upstream b { + server localhost:9082; + } + + # this is an app server protected by border patrol. If it returns a 401 + # when an attempt is made to access a protected resource, borderpatrol redirects + # to the account service login + upstream c { + server localhost:9083; + } + + # this is the account service. displays the login screen and also calls the auth service + # to get a master token and a service token + upstream account { + server localhost:9084; + } + + # Nginx Lua has no SSL support for cosockets. This is unfortunate. + # This proxies all requests to use the native NGINX request, though + # it's a little hacky and sort of dirty. + upstream token_server { + server localhost:9081; + } + + # Service mappings, map service urls to service names + init_by_lua 'service_mappings = {b="smb", c="flexd"}'; + + server { + listen 4443 default_server ssl; + root html; + + ssl_prefer_server_ciphers on; + ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH; + ssl_session_cache shared:SSL:16m; + ssl_session_timeout 10m; + ssl_certificate ../ssl/server.crt; + ssl_certificate_key ../ssl/server.key; + + # GET /session?id=foo -> memcache get + # POST /session?id=foo -> memcache add, value is request body + # PUT /session?id=foo -> memcache set, value is request body + location = /session { + internal; + set $memc_key $arg_id; + set $memc_exptime $arg_exptime; + memc_pass session_store; + } + + # DELETE /session_delete?id=foo -> memcache delete + location = /session_delete { + internal; + set $memc_key $arg_id; + memc_pass session_store; + } + + location = /auth { + internal; + content_by_lua_file '../../build/usr/share/borderpatrol/validate.lua'; + } + + location = /serviceauth { + internal; + content_by_lua_file '../../build/usr/share/borderpatrol/service_token.lua'; + } + + location = /authtoken { + internal; + rewrite ^/(.*) /api/auth/public/v1/account_token.json break; + proxy_pass http://token_server; + proxy_set_header Host $host; + } + + location = /mastertoken { + internal; + rewrite ^/(.*) /api/auth/service/v1/account_token.json break; + proxy_pass http://token_server; + proxy_set_header Host $host; + } + + location = / { + limit_req zone=auth_zone burst=25; + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; + } + + location = /logout { + content_by_lua_file '../../build/usr/share/borderpatrol/logout.lua'; + } + + location ~ /(b|c)* { + set $original_uri $uri; + rewrite ^/(.*) / break; + set $auth_token $http_auth_token; + access_by_lua_file '../../build/usr/share/borderpatrol/access.lua'; + proxy_set_header auth-token $auth_token; + proxy_pass http://$1; + proxy_intercept_errors on; + error_page 401 = @redirect; + } + + location @redirect { + content_by_lua_file '../../build/usr/share/borderpatrol/redirect.lua'; + } + + location = /health { + content_by_lua_file '../../build/usr/share/borderpatrol/health_check.lua'; + } + + location /robots.txt { + alias ../../build/usr/share/borderpatrol/robots.txt; + } + + location / { + set $auth_token $http_auth_token; + access_by_lua_file '../../build/usr/share/borderpatrol/access.lua'; + proxy_set_header auth-token $auth_token; + + # http://hostname/upstream_name/uri -> http://upstream_name/uri + rewrite ^/([^/]+)/?(.*)$ /$2 break; + proxy_pass http://$1; + proxy_redirect off; + proxy_set_header Host $host; + } + } +} + +events { + worker_connections 40; +} + +# vim: ft=conf \ No newline at end of file diff --git a/src/health_check.lua b/src/health_check.lua new file mode 100644 index 0000000..f1066f0 --- /dev/null +++ b/src/health_check.lua @@ -0,0 +1,55 @@ +-- +-- This script serves up an HTML page that displays current health of the +-- BorderPatrol. The only check, currently, is that memcache is reachable. +-- +local errors = {} +local health_check = {} + +-- print out the actual HTML +function health_check.output(errors) + ngx.header.content_type = 'text/html'; + ngx.print([[ + + + Border Patrol Health + + + ]]) + + if #errors > 0 then + ngx.print("

Errors

    ") + for i, v in ipairs(errors) do + ngx.print("
  • " .. v .. "
  • ") + end + ngx.print("
") + else + ngx.print("Everything is ok.") + end + + ngx.print([[ + + + ]]) +end + +local res = ngx.location.capture('/session?id=health_check', { method = ngx.HTTP_POST, body = os.time() }) +if not res.status == ngx.HTTP_OK then + errors[#errors+1] = "memcache add: " .. res.status .. ": " .. res.body +end + +res = ngx.location.capture('/session?id=health_check') +if not res.status == ngx.HTTP_OK then + errors[#errors+1] = "memcache get: " .. res.status .. ": " .. res.body +end + +res = ngx.location.capture('/session?id=health_check', { method = ngx.HTTP_PUT, body = os.time() }) +if not res.status == ngx.HTTP_OK then + errors[#errors+1] = "memcache set: " .. res.status .. ": " .. res.body +end + +res = ngx.location.capture('/session?id=health_check', { method = ngx.HTTP_DELETE }) +if not res.status == ngx.HTTP_OK then + errors[#errors+1] = "memcache delete: " .. res.status .. ": " .. res.body +end + +health_check.output(errors) diff --git a/src/logout.lua b/src/logout.lua new file mode 100644 index 0000000..972a028 --- /dev/null +++ b/src/logout.lua @@ -0,0 +1,37 @@ +------------------------------------------- +-- delete auth token by session id +------------------------------------------- + +local args = ngx.req.get_uri_args() +local destination = args['destination'] + +-- default to reasonable paths. +-- allow only relative paths +if not destination or string.sub(destination,1,1) ~= '/' then destination = '/' end + +local session_id = ngx.var.cookie_border_session + +if session_id then + ngx.log(ngx.DEBUG, "==== session_id: " .. session_id) + + -- expires a session + local res = ngx.location.capture('/session_delete?id=BPSID_' .. session_id, + { method = ngx.HTTP_DELETE }) + + ngx.log(ngx.DEBUG, "DELETE /session_delete?id=BPSID_" .. session_id .. " " .. res.status) + + -- expires the temporary url session + local res = ngx.location.capture('/session_delete?id=BP_URL_SID_' .. session_id, + { method = ngx.HTTP_DELETE }) + + ngx.log(ngx.DEBUG, "DELETE /session_delete?id=BP_URL_SID_" .. session_id .. " " .. res.status) + + -- unset session cookie (expires now() - 1yr) + ngx.header['Set-Cookie'] = 'border_session=' .. + '; path=/; expires=' .. ngx.cookie_time(ngx.time() - 3600 * 24 * 360) + +else + ngx.log(ngx.INFO, "==== session_id not set") +end + +ngx.redirect(destination) \ No newline at end of file diff --git a/src/redirect.lua b/src/redirect.lua new file mode 100644 index 0000000..bc30792 --- /dev/null +++ b/src/redirect.lua @@ -0,0 +1,25 @@ +local sessionid = require("sessionid") + +local original_url = ngx.var.original_uri + +-- Create session key and store the url as value (value will be updated later with master and service tokens +local session_id = sessionid.generate(); + +-- store original url in memcache via internal subrequest +local res = ngx.location.capture('/session?id=BP_URL_SID_' .. session_id .. + '&arg_exptime=' .. sessionid.EXPTIME_TMP, { body = original_url, method = ngx.HTTP_POST }) + +ngx.log(ngx.DEBUG, "==== POST /session?id=BP_URL_SID_" .. session_id .. + '&arg_exptime=' .. sessionid.EXPTIME .. " " .. res.status) + +if res.status == ngx.HTTP_CREATED then + -- set the cookie with the session_id + ngx.header['Set-Cookie'] = 'border_session=' .. session_id .. '; path=/; HttpOnly; Secure;' + -- Redirect to account service login + ngx.redirect('/account') +else + ngx.log(ngx.ERR, "==== an error occurred trying to save session: " .. res.status) + ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) +end + + diff --git a/src/robots.txt b/src/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/src/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/src/service_token.lua b/src/service_token.lua new file mode 100644 index 0000000..26a70bd --- /dev/null +++ b/src/service_token.lua @@ -0,0 +1,49 @@ +local json = require("json") +local sessionid = require("sessionid") + +------------------------------------------- +-- get service token using master token +------------------------------------------- + +local session_id = ngx.var.cookie_border_session + +if not session_id then + ngx.log(ngx.INFO, "==== access denied: no session_id") + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +ngx.log(ngx.DEBUG, "==== session_id: " .. session_id) + +if not sessionid.is_valid(session_id) then + ngx.log(ngx.INFO, "==== access denied: session id invalid " .. session_id) + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +ngx.req.read_body() +local args = ngx.req.get_post_args() +local master_token = args['mastertoken'] +local service = args['service'] + +if master_token and service then + ngx.log(ngx.DEBUG, "==== master token: " .. master_token ) + local params = {} + params['services'] = service + ngx.req.set_header("Auth-Token", master_token) + + res = ngx.location.capture('/mastertoken', { method = ngx.HTTP_POST, body = ngx.encode_args(params)}) + + ngx.log(ngx.DEBUG, "==== POST /mastertoken " .. res.status .. " " .. res.body) + ngx.req.clear_header("Auth-Token") + + -- assume any 2xx is success + if res.status >= ngx.HTTP_SPECIAL_RESPONSE then + ngx.log(ngx.DEBUG, "==== error getting service token using master token: ") + ngx.exit(ngx.HTTP_BAD_REQUEST) + end +else + ngx.log(ngx.DEBUG, "==== master token missing: ") + ngx.exit(ngx.HTTP_BAD_REQUEST) +end +ngx.say(res.body) +ngx.exit(ngx.HTTP_OK) + diff --git a/src/sessionid.lua b/src/sessionid.lua new file mode 100644 index 0000000..489fac3 --- /dev/null +++ b/src/sessionid.lua @@ -0,0 +1,275 @@ +-- session_id module to issue and validate signed session ids +local crypto = require("crypto") + +local module = {} + +-- record expiration time in memcached +local EXPTIME = 60 * 60 +local EXPTIME_TMP = 60 * 60 * 24 * 7 + +-- defines how often secrets will be rotated +local SECRETS_EXP_INTERVAL = 60 * 60 * 24 + +-- internal session cookie lifetime to prevent unbound sessions +local SESSION_COOKIE_LIFETIME = SECRETS_EXP_INTERVAL * 2 + +-- lease lifetime (short-living) +local SECRETS_LEASE_INTERVAL = 5 + +-- signature length - used for basic validation +local HMAC_SHA1_SIGN_LENGTH = 20 + +-- number of random bytes generated for session ids +local DATA_LENGTH = 16 + +-- secret length - we keep 2 secrets in memory +local KEY_LENGTH = 64 + +-- stores the time when to check whether we need to rotate secrets +local ts_next_refresh = -1 + +-- encode string and make it url-safe +local function encode(str, urlsafe) + + local enc_str = ngx.encode_base64(str) + if urlsafe then + enc_str = string.gsub(enc_str, "+", "-") -- plus -> dash + enc_str = string.gsub(enc_str, "/", "_") -- slash to underscore + enc_str = string.gsub(enc_str, "=", "*") -- equal to star + end + + return enc_str +end + +-- decode string +-- returns empty string if data is not base64 encoded +local function decode(str, urlsafe) + + local dec_str = str + if urlsafe then + dec_str = string.gsub(dec_str, "-", "+") -- plus -> dash + dec_str = string.gsub(dec_str, "_", "/") -- slash to underscore + dec_str = string.gsub(dec_str, "*", "=") -- equal to star + end + + return ngx.decode_base64(dec_str) +end + +-- serializes secret into string +local function serialize_secret(secret) + local str + if secret and type(secret) == "table" then + str = secret.data .. ":" .. secret.ts + end + return str +end + +-- deserializes secret string into a table +local function deserialize_secret(str) + local secret + if str and type(str) == "string" then + local startPos, endPos, data, ts = string.find(str, "(.+):(.+)") + secret = {} + secret.data = data + secret.ts = tonumber(ts) + end + return secret +end + +-- +-- function to save secrets in SHM and memcached +-- secret1 mandatory +-- secret2 optional +-- +local function save_secrets(secret1, secret2) + + local secret1_str = serialize_secret(secret1) + local secret2_str = serialize_secret(secret2) + + -- persist in memcached + local res = ngx.location.capture('/session?id=BPS1', + { body = secret1_str, method = ngx.HTTP_PUT }) + if res.status ~= ngx.HTTP_CREATED then + ngx.log(ngx.WARN, "==== failed to persist secret1 " .. secret1_str .. " " .. res.status) + else + ngx.log(ngx.DEBUG, "==== secret1 persisted successfully " .. secret1_str .. " " .. res.status) + end + + -- secret2 is optional + if secret2_str then + -- persist in memcached + res = ngx.location.capture('/session?id=BPS2', + { body = secret2_str, method = ngx.HTTP_PUT }) + if res.status ~= ngx.HTTP_CREATED then + ngx.log(ngx.WARN, "==== failed to persist secret2 " .. secret2_str .. " " .. res.status) + else + ngx.log(ngx.DEBUG, "==== secret2 persisted successfully " .. secret2_str .. " " .. res.status) + end + end +end + +-- +-- function to retrieve secrets, secret1 is current, secret2 is rotated +-- +local function get_secrets() + + local secret1, secret2 + + local res1, res2 = ngx.location.capture_multi{ + { '/session?id=BPS1', { method = ngx.HTTP_GET } }, + { '/session?id=BPS2', { method = ngx.HTTP_GET } }, + } + + if res1.status >= ngx.HTTP_INTERNAL_SERVER_ERROR then + ngx.log(ngx.ERR, "==== could not fetch secrets - memcached down?") + ngx.exit(res1.status) + end + + if res1.status == ngx.HTTP_OK then + secret1 = deserialize_secret(res1.body) + ts_next_refresh = secret1.ts + SECRETS_EXP_INTERVAL -- update to the time when the cookie expired + elseif res1.status == ngx.HTTP_NOT_FOUND then + ngx.log(ngx.ERR, "==== secret1 not found - did memcached just restart?") + -- trigger key refresh in case memcached got restarted + ts_next_refresh = 0 + else + ngx.log(ngx.ERR, "==== failed to retrieve secret1 " .. res1.status) + end + + if res2.status == ngx.HTTP_OK then + secret2 = deserialize_secret(res2.body) + else + ngx.log(ngx.WARN, "==== failed to retrieve secret2 - that's prob okay in case secrets have not been rotated yet. " .. res2.status) + end + + return secret1, secret2 +end + +-- +-- function to refresh secrets for hmac signing +-- +local function refresh_keys_if_required() + + -- initial check + if (ts_next_refresh == -1) then + local secret1, secret2 = get_secrets() + if secret1 then + ts_next_refresh = secret1.ts + SECRETS_EXP_INTERVAL + else + ts_next_refresh = 0 + end + end + + if (ngx.time() > ts_next_refresh) then + ngx.log(ngx.DEBUG, "==== it's time to refresh keys...") + + local res = ngx.location.capture("/session?id=BP_LEASE", { method = ngx.HTTP_GET }) + if res.status == ngx.HTTP_NOT_FOUND then + + local res = ngx.location.capture("/session?id=BP_LEASE&exptime=" .. SECRETS_LEASE_INTERVAL, + { body = "1", method = ngx.HTTP_POST }) + if res.status == ngx.HTTP_CREATED then + + local secret1, secret2 = get_secrets() + + -- generate new secret + local new_secret = {} + new_secret.data = ngx.encode_base64(crypto.rand.bytes(KEY_LENGTH)) + new_secret.ts = ngx.time(); + + -- persist new secrets + save_secrets(new_secret,secret1) + + res = ngx.location.capture("/session_delete?id=BP_LEASE", { method = ngx.HTTP_GET }) + ngx.log(ngx.DEBUG, "==== lease deleted " .. res.status) + + ts_next_refresh = new_secret.ts + SECRETS_EXP_INTERVAL + + elseif res.status == ngx.HTTP_OK then + ngx.log(ngx.DEBUG, "=== lease to update keys already taken - concurrent update?") + else + ngx.log(ngx.WARN, "==== failed to acquire lease " .. res.status) + end + else + ngx.log(ngx.WARN, "==== refresh keys not needed yet") + end + end +end + +-- +-- generate session_id +-- +local function generate() + + refresh_keys_if_required() + + -- create 128 bit of random bytes + local data = crypto.rand.bytes(DATA_LENGTH) + local ts = ngx.time() + local secret1, secret2 = get_secrets() + + -- we could also use 'crypto' for 'digesting' but benchmark shows it's slightly + -- slower: crypto.hmac.digest("sha1", data, secret1, true) + local signature = ngx.hmac_sha1(secret1.data, data .. ts) + local session_id = encode(data, true) .. ":" .. ts .. ":" .. encode(signature, true) + + return session_id +end + +-- +-- function to validate session_id +-- +local function is_valid(session_id) + + refresh_keys_if_required() + + -- parse session id + local startPos, endPos, data, ts, signature = string.find(session_id, "(.+):(.+):(.+)") + ts = tonumber(ts) + + -- check for the obvious + if not data or not signature or not ts then + return false + end + + -- check whether the cookie expired already + if ngx.time() > ts + SESSION_COOKIE_LIFETIME then + return false + end + + -- decode + data = decode(data, true) + signature = decode(signature, true) + + -- check whether data or signature became nil (happens in case it couldn't be decoded properly) + if not data or #data ~= DATA_LENGTH or not signature or #signature ~= HMAC_SHA1_SIGN_LENGTH then + return false + end + + -- re-compute signature + local secret1, secret2 = get_secrets() + if not secret1 then + -- did memcache just restarted? + return false + else + local computed_signature = ngx.hmac_sha1(secret1.data, data .. ts) + + local match = computed_signature == signature + if not match and secret2 then + -- fallback - check with secret2 + -- TBD: issue fresh session cookie to prevent session loss after secrets + -- get rotated, a bit overkill for now + computed_signature = ngx.hmac_sha1(secret2.data, data .. ts) + match = computed_signature == signature + end + + return match + end +end + +module.generate = generate +module.is_valid = is_valid +module.EXPTIME = EXPTIME +module.EXPTIME_TMP = EXPTIME_TMP + +return module \ No newline at end of file diff --git a/src/ssl/README.md b/src/ssl/README.md new file mode 100644 index 0000000..e1a40a7 --- /dev/null +++ b/src/ssl/README.md @@ -0,0 +1,11 @@ +This folder contains self-signed SSL key and certs which will be used for local testing with your browser: + +Generate new ones via (won't be needed though): + + openssl genrsa -des3 -out server.key 1024 + openssl req -new -key server.key -out server.csr + cp server.key server.key.org + openssl rsa -in server.key.org -out server.key + openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt + +Additional details can be found here. \ No newline at end of file diff --git a/src/ssl/server.crt b/src/ssl/server.crt new file mode 100644 index 0000000..1463838 --- /dev/null +++ b/src/ssl/server.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTzCCAbgCCQDst2eQl9PzIzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQGEwJV +UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xFjAUBgNVBAoT +DUxvb2tvdXQsIEluYy4xIDAeBgNVBAMTF3NlbGYtc2lnbmVkLmxvb2tvdXQuY29t +MB4XDTEzMDkxNzE3MTIzNFoXDTE0MDkxNzE3MTIzNFowbDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRYwFAYDVQQKEw1M +b29rb3V0LCBJbmMuMSAwHgYDVQQDExdzZWxmLXNpZ25lZC5sb29rb3V0LmNvbTCB +nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAnqcN6ZWG1q3E2HDU5Z1OM9HIAV64 +1XBN7JW/+CKR+GOpuRGqlwSQ4QYmB+TZJnB+p0f6whKnb5Cch33m0oSP//NCy0Oz +/TgP33T5bPu+a7/MUIYjpYLwsiD4fWb1AALbJ+ZxqIinouiJUy79tpWU8SjrTxRO +ubLY3FWHKmpSyQ0CAwEAATANBgkqhkiG9w0BAQUFAAOBgQBDKshY0e8H/nu2UaF4 +TV41QX6g6kPEbEGDGlmyuP5n5gDAD0trdBFZpPCRCnoMGObD9InQGQ/ExVpyvvle +0AcRl0Gan6C/zv9brCTf4Ks0Y/s8u9YwmEA+qB5W020Y01GQ+WfNtlM67X2I2j7G +N0AE28/IALl4c1BUwdHs2dVN7A== +-----END CERTIFICATE----- diff --git a/src/ssl/server.key b/src/ssl/server.key new file mode 100644 index 0000000..830f532 --- /dev/null +++ b/src/ssl/server.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCepw3plYbWrcTYcNTlnU4z0cgBXrjVcE3slb/4IpH4Y6m5EaqX +BJDhBiYH5NkmcH6nR/rCEqdvkJyHfebShI//80LLQ7P9OA/fdPls+75rv8xQhiOl +gvCyIPh9ZvUAAtsn5nGoiKei6IlTLv22lZTxKOtPFE65stjcVYcqalLJDQIDAQAB +AoGAJNIMjoufca9+oeT95BRwE+K6EmdTamXYD/JpTUNosUcgGs2Y09fBcBgnN2nL +Y/pzyosQDX6a0W+0hFWZ/n25lYXgN9gWK5y9xxE/WGuZCgFEcGrPzvXM/r1zSh1s +N10dmpQJbIPEfgOVDZuVokk2+FUJZUxDLbGghPhcWElJUEECQQDPXGKiV5ZVl2U9 +wm3VfhT2iKvpsod5573PSeUs12PtExxwIpZJcD54RRrgJWUtubTWBlSgF02egkko +PihrDofpAkEAw93UYKDoxW42tLvVMMvxOPsshHh+P4A+XjzKXxrNceXxHF0h+xFB +w+6ywoVGnpp6Om08mw3zhaQfTBj2DaWlhQJAKL1x84tZ0f8ouPWWNrfKzpUTkZqt +21mYhT1zdVfsHgv/LljdRhhzbZXGLfuq4Uz3JoWf4sQxT88xKGLt9fqo4QJANzcS +xsa1t+pw+5Qz7lSfxOtxykpZdLdHXbOPbS4WGnSy+sb6bFeaDYz90b5WgSGVMWFY +A3H0Y4k31XD39DLtLQJBALODq7HHEsKAfZTvYOEVmok7EfMV11Cvt9nrAnHtuFK1 +Nw+eYNPCpckN8qVUzsiUHfWdtAYUuvMzcpEF3M2oVMw= +-----END RSA PRIVATE KEY----- diff --git a/src/validate.lua b/src/validate.lua new file mode 100644 index 0000000..18081a2 --- /dev/null +++ b/src/validate.lua @@ -0,0 +1,83 @@ +local json = require("json") +local sessionid = require("sessionid") + +------------------------------------------- +-- Lookup auth token by session id +------------------------------------------- + +local session_id = ngx.var.cookie_border_session + +if not session_id then + ngx.log(ngx.INFO, "==== access denied: no session_id") + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +ngx.log(ngx.DEBUG, "==== session_id: " .. session_id) + +if not sessionid.is_valid(session_id) then + ngx.log(ngx.INFO, "==== access denied: session id invalid " .. session_id) + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +local auth_token = nil +local all_tokens_json = nil + +local res = ngx.location.capture('/session?id=BPSID_' .. session_id) +ngx.log(ngx.DEBUG, "GET /session?id=BPSID_" .. session_id .. " " .. + res.status) + +local all_tokens = nil + +if res.status == ngx.HTTP_OK then + all_tokens_json = res.body + all_tokens = json.decode(all_tokens_json, {nothrow = true}) + + if all_tokens then + -- get service name from uri + local service_uri = string.match(ngx.var.request_uri,"^/([^/]+)") + local service = nil + if service_uri then + service = service_mappings[service_uri] + end + + auth_token = all_tokens['service_tokens'][service] + + if not auth_token then + ngx.log(ngx.INFO, "==== token not found in session for service : " .. service_uri) + master_token = all_tokens['auth_service'] + if master_token then + local params = {} + params['mastertoken'] = master_token + params['service'] = service + res = ngx.location.capture('/serviceauth', { method = ngx.HTTP_POST, body = ngx.encode_args(params) }) + + -- TODO Check response status (in this case, don't do anything, treat it as missing token) + local specific_token_json = res.body + auth_token = json.decode(specific_token_json, {nothrow = true})['service_tokens'][service] + if auth_token then + all_tokens['service_tokens'][service] = auth_token + all_tokens_json = json.encode(all_tokens) + end + ngx.log(ngx.INFO, "==== retrieved service token for service: " .. service .. " " .. auth_token) + end + end + -- reset the auth_token TTL to maintain a rolling session window + res = ngx.location.capture('/session?id=BPSID_' .. session_id .. + "&exptime=" .. sessionid.EXPTIME, { body = all_tokens_json, method = ngx.HTTP_PUT }) + if res.status ~= ngx.HTTP_CREATED then + ngx.log(ngx.WARN, "==== failed to refresh session w/ id " .. session_id .. + " " .. res.status) + end + end +end + +if not auth_token then + ngx.log(ngx.INFO, "==== access denied: no auth token for session_id " .. + session_id) + ngx.exit(ngx.HTTP_UNAUTHORIZED) +end + +-- If we made it this far, we're good. Inject the Auth-Token header +ngx.header['Auth-Token'] = auth_token +ngx.log(ngx.INFO, "==== request auth header set") +ngx.exit(ngx.HTTP_OK) diff --git a/t/.gitkeep b/t/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/t/access.t b/t/access.t new file mode 100644 index 0000000..2a32138 --- /dev/null +++ b/t/access.t @@ -0,0 +1,91 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +plan tests => $Test::Nginx::Socket::RepeatEach * 2 * blocks(); + +run_tests(); + +__DATA__ + +=== TEST 1: test w/ auth-token present in client request +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; + +upstream b { + server 127.0.0.1:$TEST_NGINX_SERVER_PORT; # self +} + +--- config +location /testpath { + echo_status 200; + echo_duplicate 1 $echo_client_request_headers; + echo 'everything is ok'; + echo_flush; +} +location /auth { + echo_status 200; + echo_flush; +} +location /b/testpath { + set $auth_token $http_auth_token; + access_by_lua_file '../../build/usr/share/borderpatrol/access.lua'; + proxy_set_header auth-token $auth_token; + + # http://hostname/upstream_name/uri -> http://upstream_name/uri + rewrite ^/([^/]+)/?(.*)$ /$2 break; + proxy_pass http://$1; + proxy_redirect off; + proxy_set_header Host $host; +} +--- request +GET /b/testpath +--- more_headers +auth-token: tokentokentokentoken +--- error_code: 200 +--- response_body_like +auth-token: tokentokentoken.+everything is ok$ + +=== TEST 2: test w/o auth-token not present in client request but with valid session +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb",s="flexd"}'; +upstream b { + server 127.0.0.1:$TEST_NGINX_SERVER_PORT; # self +} +--- config +location /testpath { + echo_status 200; + echo_duplicate 1 $echo_client_request_headers; + echo 'everything is ok'; + echo_flush; +} +location /auth { + internal; + echo_status 200; + more_set_headers 'Auth-Token: tokentokentokentoken'; + echo_flush; +} +location /b/testpath { + set $auth_token $http_auth_token; + access_by_lua_file '../../build/usr/share/borderpatrol/access.lua'; + proxy_set_header auth-token $auth_token; + + # http://hostname/upstream_name/uri -> http://upstream_name/uri + rewrite ^/([^/]+)/?(.*)$ /$2 break; + proxy_pass http://$1; + proxy_redirect off; + proxy_set_header Host $host; +} +--- request +GET /b/testpath +--- more_headers +Cookie: border_session=this-is-a-session-id # not checked here! +--- error_code: 200 +--- response_body_like +auth-token: tokentokentoken.+everything is ok$ \ No newline at end of file diff --git a/t/authorize.t b/t/authorize.t new file mode 100644 index 0000000..41016b7 --- /dev/null +++ b/t/authorize.t @@ -0,0 +1,271 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (2 * blocks()) - 1; + +run_tests(); + +__DATA__ + +=== TEST 0: initialize memcached +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; +} +--- request +GET /setup +--- more_headers +Content-type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like +OK\r +STORED\r +STORED\r + +=== TEST 1: test successful login +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; + echo_subrequest POST '/memc_setup?key=BP_URL_SID_MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg*' -b '/b'; + echo_status 200; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /authorize { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; +} +location /account { + internal; + echo_status 200; + echo '{"auth_service": "tokentokentokentoken", "service_tokens": {"smb": "tokentokentokentoken"}}'; + echo_flush; +} +--- request eval +["GET /setup", "POST /authorize + username=foo&password=bar"] +--- more_headers +Content-type: application/x-www-form-urlencoded +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +---- response_headers_like +Set-Cookie: border_session=.+:.+; path=/; HttpOnly; Secure;$ +Location: http://localhost(?::\d+)?/b$ +--- error_code eval +[200,302] + +=== TEST 2: test unsuccessful login +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; + echo_subrequest POST '/memc_setup?key=BP_URL_SID_MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg*' -b '/b'; + echo_status 200; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /authorize { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; +} +location /account { + internal; + echo_status 403; + echo_flush; +} +--- request eval +["GET /setup", "POST /authorize + username=foo&password=bar"] +--- more_headers +Content-type: application/x-www-form-urlencoded +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +---- response_headers_like +Set-Cookie: border_session=.+:.+; path=/; HttpOnly; Secure;$ +Location: http://localhost(?::\d+)?/b$ +--- error_code eval +[200,302] + +=== TEST 3: test failed login with memcached down +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /session { # memcached + internal; + echo_status 502; # simulate memcached down + echo_flush; +} +location /authorize { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; +} +location /account { + internal; + echo_status 200; + echo '{"auth_service": "tokentokentokentoken", "service_tokens": {"smb": "tokentokentokentoken"}}'; + echo_flush; +} +--- request eval +"POST /authorize +username=foo&password=bar" +--- more_headers +Content-type: application/x-www-form-urlencoded +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +---- response_headers_like +Set-Cookie: border_session=.+:.+; path=/; HttpOnly; Secure;$ +Location: http://localhost(?::\d+)?/b$ +--- error_code: 502 + +=== TEST 4: test failed login with Account Service down +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; + echo_subrequest POST '/memc_setup?key=BP_URL_SID_MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg*' -b '/b'; + echo_status 200; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /authorize { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; +} +location /account { + internal; + echo_status 500; + echo_flush; +} +--- request eval +["GET /setup", "POST /authorize + username=foo&password=bar"] +--- more_headers +Content-type: application/x-www-form-urlencoded +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +---- response_headers_like +Set-Cookie: border_session=.+:.+; path=/; HttpOnly; Secure;$ +Location: http://localhost(?::\d+)?/b$ +--- error_code eval +[200,302] + +=== TEST 5: test invalid service url +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; + echo_subrequest POST '/memc_setup?key=BP_URL_SID_MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg*' -b '/x'; + echo_status 200; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /authorize { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/authorize.lua'; +} +location /account { + internal; + echo_status 403; + echo_flush; +} +--- request eval +["GET /setup", "POST /authorize + username=foo&password=bar"] +--- more_headers +Content-type: application/x-www-form-urlencoded +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +---- response_headers_like +Set-Cookie: border_session=.+:.+; path=/; HttpOnly; Secure;$ +Location: http://localhost(?::\d+)?/b$ +--- error_code eval +[200,302] diff --git a/t/borderpatrol.god b/t/borderpatrol.god new file mode 100644 index 0000000..936d444 --- /dev/null +++ b/t/borderpatrol.god @@ -0,0 +1,69 @@ +# vim: ft=ruby + +ROOT_PATH = Dir.pwd +TEST_PATH = File.join(ROOT_PATH, 't') +SERVICE_PATH = File.join(TEST_PATH, 'services') +SERVER_PATH = File.join(TEST_PATH, 'servroot') +LOG_PATH = File.join(SERVER_PATH, 'logs') + +GROUP_NAME = 'borderpatrol' + +# Watch the api service +God.watch do |w| + w.name = 'api_service' + w.group = GROUP_NAME + w.dir = SERVICE_PATH + w.start = 'bundle exec shotgun api_service.rb -p 9082' + w.log = File.join(LOG_PATH, 'api_service.out') + w.keepalive +end + +# Watch the 2nd api service +God.watch do |w| + w.name = 'api_service2' + w.group = GROUP_NAME + w.dir = SERVICE_PATH + w.start = 'bundle exec shotgun api_service2.rb -p 9083' + w.log = File.join(LOG_PATH, 'api_service2.out') + w.keepalive +end + +# Watch the account service +God.watch do |w| + w.name = 'account_service' + w.group = GROUP_NAME + w.dir = SERVICE_PATH + w.start = 'bundle exec shotgun account_service.rb -p 9084' + w.log = File.join(LOG_PATH, 'account_service.out') + w.keepalive +end + +# Watch the token server +God.watch do |w| + w.name = 'token_service' + w.group = GROUP_NAME + w.dir = SERVICE_PATH + w.start = 'bundle exec shotgun auth_service.rb -p 9081' + w.log = File.join(LOG_PATH, 'token_service.out') + w.keepalive +end + +God.watch do |w| + w.name = 'memcache' + w.group = GROUP_NAME + w.dir = SERVER_PATH + w.start = 'memcached -vvv' + w.log = File.join(LOG_PATH, 'memcached.out') + w.keepalive +end + +God.watch do |w| + w.name = 'nginx' + w.group = GROUP_NAME + w.dir = SERVER_PATH + w.start = "#{ROOT_PATH}/build/usr/sbin/borderpatrol -g 'error_log #{LOG_PATH}/error.log;' -p #{SERVER_PATH} -c #{ROOT_PATH}/build/etc/borderpatrol/sites-available/borderpatrol.conf.sample" + w.log = File.join(LOG_PATH, 'nginx.out') + w.keepalive +end + + diff --git a/t/health.t b/t/health.t new file mode 100644 index 0000000..438694f --- /dev/null +++ b/t/health.t @@ -0,0 +1,33 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (1 * blocks()); + +run_tests(); + +__DATA__ + +=== TEST 1: heath controller +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config + location = /session { + set $memc_key $arg_id; + set $memc_exptime 10; + + memc_cmds_allowed get set add delete; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; + } + + location /health { + content_by_lua_file '../../build/usr/share/borderpatrol/health_check.lua'; + } +--- request + GET /health +--- error_code: 200 diff --git a/t/logout.t b/t/logout.t new file mode 100644 index 0000000..c6dc23d --- /dev/null +++ b/t/logout.t @@ -0,0 +1,77 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (2 * blocks()); + +run_tests(); + +__DATA__ + +=== TEST 1: test logout w/o destination and w/o session_id +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /logout { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/logout.lua'; +} +--- request +GET /logout +--- error_code: 302 +--- response_headers_like: Location: http://localhost(?::\d+)?/$ + +=== TEST 2: test logout w/ relative destination and w/o session_id +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /logout { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/logout.lua'; +} +--- request +GET /logout?destination=/somepath +--- error_code: 302 +--- response_headers_like: Location: http://localhost(?::\d+)?/somepath$ + +=== TEST 3: test logout w/ absolute destination and w/o session_id +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /logout { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/logout.lua'; +} +--- request +GET /logout?destination=http://www.evil.org +--- error_code: 302 +--- response_headers_like: Location: http://localhost(?::\d+)?/$ + +=== TEST 4: test logout w/ session_id +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /logout { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/logout.lua'; +} +location /session_delete { # memcached + internal; + echo_status 400; # simulates successful write into memcached + echo_flush; +} +--- request +GET /logout +--- more_headers +Cookie: border_session=wfxLNdl2BrLN9NVuQ9_wiA**:4lcaas0Onjxsn2D6kDVPTw** +--- error_code: 302 +--- response_headers_like +Set-Cookie: border_session=; path=/; expires=.+GMT$ +--- response_headers_like: Location: http://localhost(?::\d+)?/$ diff --git a/t/redirect.t b/t/redirect.t new file mode 100644 index 0000000..8d235ce --- /dev/null +++ b/t/redirect.t @@ -0,0 +1,105 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (2 * blocks()) - 1; + +run_tests(); + +__DATA__ + +=== TEST 0: initialize memcached +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; +} +--- request +GET /setup +--- more_headers +Content-type: application/x-www-form-urlencoded +--- error_code: 200 +--- response_body_like +OK\r +STORED\r +STORED\r + +=== TEST 1: test successful redirect +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /session { + internal; + set $memc_key $arg_id; + set $memc_value $arg_val; + set $memc_exptime $arg_exptime; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /redirect { + content_by_lua_file '../../build/usr/share/borderpatrol/redirect.lua'; +} + +--- request eval +"POST /redirect" +--- more_headers +Content-type: application/x-www-form-urlencoded +--- error_code: 302 +--- response_headers_like +Location: http://localhost(?::\d+)?/account$ + +=== TEST 2: test memcached down +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {b="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location /session { # memcached + internal; + echo_status 502; # simulate memcached down + echo_flush; +} +location /redirect { + content_by_lua_file '../../build/usr/share/borderpatrol/redirect.lua'; +} +--- request eval +"POST /redirect" +--- more_headers +Content-type: application/x-www-form-urlencoded +--- error_code: 502 + diff --git a/t/service_token.t b/t/service_token.t new file mode 100644 index 0000000..8d2d7fd --- /dev/null +++ b/t/service_token.t @@ -0,0 +1,166 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (2 * blocks()) -3; + +run_tests(); + +__DATA__ + +=== TEST 0: test getting service token with valid master token and valid service name +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /mastertoken { + echo_status 200; + echo '{smb: "tokentokentokentoken"}'; + echo_flush; +} + +location /serviceauth { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/service_token.lua'; +} + +--- request eval +"POST /serviceauth +mastertoken=DEADBEEF&service=smb" +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +Content-type: application/x-www-form-urlencoded + +--- response_body_like +{smb: "tokentokentokentoken"} + +--- error_code: 200 + +=== TEST 1: test attempt to get service token with valid master token and invalid service name +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /mastertoken { + echo_status 400; + echo_flush; +} + +location /serviceauth { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/service_token.lua'; +} + +--- request eval +"POST /serviceauth +mastertoken=DEADBEEF&service=junkservice" +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +Content-type: application/x-www-form-urlencoded + +--- error_code: 400 + +=== TEST 2: test attempt to get service token with nil master token +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /mastertoken { + echo_status 400; + echo_flush; +} + +location /serviceauth { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/service_token.lua'; +} + +--- request eval +"POST /serviceauth +service=junkservice" +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +Content-type: application/x-www-form-urlencoded + +--- error_code: 400 + +=== TEST 3: test attempt to get service token with valid master token but missing service name +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /mastertoken { + echo_status 400; + echo_flush; +} + +location /serviceauth { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/service_token.lua'; +} + +--- request eval +"POST /serviceauth +service=junkservice" +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +Content-type: application/x-www-form-urlencoded + +--- error_code: 400 + + diff --git a/t/services/account_service.rb b/t/services/account_service.rb new file mode 100644 index 0000000..e99c7e6 --- /dev/null +++ b/t/services/account_service.rb @@ -0,0 +1,89 @@ +require 'sinatra' +require 'json' +require "net/http" +require "uri" + +KEYMASTER_URI = 'http://localhost:9081/api/auth/service/v1/account_master_token.json' + + +post '/' do + $stderr.write "apiserver #{request.url}\n" + service = request['service'] + username = request['username'] + password = request['password'] + + uri = URI.parse(KEYMASTER_URI) + params = {'s'=> service, 'e' => username, 'p' => password} + response = Net::HTTP.post_form(uri, params) + $stderr.write "keymaster status was: #{response.code} response body was #{response.body}\n" + if response.code == '200' + $stderr.write "account service returning: #{response.body}\n" + content_type :json + response.body + else + $stderr.write "Unable to Authorize user!\n" + halt 401, 'Unable to Authorize user!' + end +end + +get '/' do + $stderr.write "apiserver #{request.url}\n" + haml :login, :content_type => 'text/html' +end + +get '/password' do + $stderr.write "apiserver #{request.url}\n" + haml :password, :content_type => 'text/html' +end + +__END__ + +@@ layout +%html + %head + %title + Account Service + %body{:style => 'text-align: center'} + = yield + +@@ index +%h1 Welcome to the Account Service! +%a{:href => '/logout?destination=/b/'} + logout + +@@ loggedout +%h1 Oops, You are not logged in. +%a{:href => '/b/login'} + login + +@@ login +%h1 + ACCOUNT SERVICE LOGIN + +%form{:action => "/", :method => 'post'} + %label + Username + %input{:name => "username", :type => "text", :value => "user@example.com"} + %br/ + %label + Password + %input{:name => "password", :type => "password", :value => "password"} + %br/ + %input{:name => "service", :type => "hidden", :value => "smb"} + %input{:type => "submit", :name => "login", :value => "login"} + +@@ password +%h1 + THIS IS THE ACCOUNT MANAGEMENT PAGE + +%form{:action => "/", :method => 'post'} + %label + Username + %input{:name => "username", :type => "text", :value => "user@example.com"} + %br/ + %label + Password + %input{:name => "password", :type => "password", :value => "password"} + %br/ + %input{:name => "service", :type => "hidden", :value => "smb"} + %input{:type => "submit", :name => "login", :value => "login"} \ No newline at end of file diff --git a/t/services/api_service.rb b/t/services/api_service.rb new file mode 100644 index 0000000..b35a1d6 --- /dev/null +++ b/t/services/api_service.rb @@ -0,0 +1,48 @@ +require 'sinatra' + +["/", "/first/second"].each do |path| + get path do + token = request.env['HTTP_AUTH_TOKEN'] + $stderr.write "apiserver #{request.url} token = #{token}\n" + + if token != 'LIVEKALESMB' + halt 401, 'Ooops, request not authenticated. Did you login?' + #haml :loggedout, :content_type => 'text/html' + else + haml :index, :content_type => 'text/html' + end + end +end + +get '/login' do + $stderr.write "apiserver #{request.url}\n" + haml :login, :content_type => 'text/html' +end + +get '/unrestricted' do + 'This is an unsecured resource.' +end + +get '/unrestricted/1' do + 'This is an unsecured resource.' +end + +__END__ + +@@ layout +%html + %head + %title + Device Login + %body{:style => 'text-align: center'} + = yield + +@@ index +%h1 Welcome to the First Server! +%a{:href => '/logout?destination=/b/'} + logout + +@@ loggedout +%h1 Oops, You are not logged in. +%a{:href => '/b'} + login \ No newline at end of file diff --git a/t/services/api_service2.rb b/t/services/api_service2.rb new file mode 100644 index 0000000..19c9495 --- /dev/null +++ b/t/services/api_service2.rb @@ -0,0 +1,48 @@ +require 'sinatra' + +["/", "/first/second"].each do |path| + get path do + token = request.env['HTTP_AUTH_TOKEN'] + $stderr.write "apiserver2 #{request.url} token = #{token}\n" + + if token != 'LIVEKALEFLEXD' + halt 401, 'Ooops, request not authenticated. Did you login?' + #haml :loggedout, :content_type => 'text/html' + else + haml :index, :content_type => 'text/html' + end + end +end + +get '/login' do + $stderr.write "apiserver2 #{request.url}\n" + haml :login, :content_type => 'text/html' +end + +get '/unrestricted' do + 'This is an unsecured resource.' +end + +get '/unrestricted/1' do + 'This is an unsecured resource.' +end + +__END__ + +@@ layout +%html + %head + %title + Device Login + %body{:style => 'text-align: center'} + = yield + +@@ index +%h1 Welcome to the Second Server! +%a{:href => '/logout?destination=/c/'} + logout + +@@ loggedout +%h1 Oops, You are not logged in. +%a{:href => '/c'} + login \ No newline at end of file diff --git a/t/services/auth_service.rb b/t/services/auth_service.rb new file mode 100644 index 0000000..c54c6e8 --- /dev/null +++ b/t/services/auth_service.rb @@ -0,0 +1,49 @@ +require 'sinatra' +require 'json' + +REQUIRED_PARAMS = [:e, :p, :s] + +# There are 2 possible scenarios +# Get a service token by itself +# wget -S --post-data "e=user@example.com&p=password&s=smb" http://localhost:9081/api/auth/service/v1/account_master_token.json + +# Get a list of service tokens (using a username and password) +# wget -S --post-data "e=user@example.com&p=password&s=smb,flexd" http://localhost:9081/api/auth/service/v1/account_master_token.json +post '/api/auth/service/v1/account_master_token.json' do + $stderr.write "apiserver #{request.url} params = #{params}\n" + REQUIRED_PARAMS.each do |r| + (status 500 and return) unless params.include?(r.to_s) + end + + if params[:e] == 'user@example.com' && params[:p] == 'password' + resp = build_tokens(params[:s]) + resp['auth_service'] = 'DEADBEEF' + content_type :json + resp.to_json + else + status 401 + end +end + +post '/api/auth/service/v1/account_token.json' do + $stderr.write "authserver #{request.url}\n" + $stderr.write "authserver master token #{request.env['HTTP_AUTH_TOKEN']}\n" + (status 500 and return) unless params.include?('services') + master_token = request.env['HTTP_AUTH_TOKEN'] + (status 401 and return) unless master_token == 'DEADBEEF' + (status 401 and return) unless params[:services] !~ /auth_service/ + $stderr.write "authserver service = #{params.inspect}\n" + resp = build_tokens(params[:services]) + $stderr.write "authserver RETURNING #{resp.inspect}\n" + content_type :json + resp.to_json +end + +def build_tokens(services) + resp = {'service_tokens' => {}} + $stderr.write resp + services.split(',').map do |service_name| + resp['service_tokens'][service_name] = "LIVEKALE#{service_name.upcase}" + end + resp +end diff --git a/t/session.t b/t/session.t new file mode 100644 index 0000000..22f14ae --- /dev/null +++ b/t/session.t @@ -0,0 +1,128 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +plan tests => $Test::Nginx::Socket::RepeatEach * 2 * blocks(); + +run_tests(); + +__DATA__ + +=== TEST 1: basic expiry +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config + location /exptime { + echo 'flush_all'; + echo_location '/memc?cmd=flush_all'; + + echo 'set foo BAR'; + echo_subrequest PUT '/memc?key=foo&exptime=1' -b BAR; + + echo 'get foo - 0 sec'; + echo_location '/memc?key=foo'; + echo; + + echo_blocking_sleep 1.1; + + echo 'get foo - 1.1 sec'; + echo_location '/memc?key=foo'; + } + location /memc { + echo_before_body "status: $echo_response_status"; + echo_before_body "exptime: $memc_exptime"; + + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + set $memc_exptime $arg_exptime; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; + } +--- request + GET /exptime +--- response_body_like +^flush_all +status: 200 +exptime: +OK\r +set foo BAR +status: 201 +exptime: 1 +STORED\r +get foo - 0 sec +status: 200 +exptime: +BAR +get foo - 1\.1 sec +status: 404 +exptime: +.*?404 Not Found.*$ + +=== TEST 2: set and reset exptime +--- config + location /exptime { + echo 'flush_all'; + echo_location '/memc?cmd=flush_all'; + + echo 'set foo BAR'; + echo_subrequest PUT '/memc?key=foo&exptime=1' -b BAR; + + echo 'get foo - 0 sec'; + echo_location '/memc?key=foo'; + echo; + + echo 'set foo BAZ'; + echo_subrequest PUT '/memc?key=foo&exptime=2' -b BAR; + + echo_blocking_sleep 1; + + echo 'get foo - 1 sec'; + echo_location '/memc?key=foo'; + echo; + + echo_blocking_sleep 1.1; + + echo 'get foo - 2 sec'; + echo_location '/memc?key=foo'; + } + location /memc { + echo_before_body "status: $echo_response_status"; + echo_before_body "exptime: $memc_exptime"; + + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + set $memc_exptime $arg_exptime; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; + } +--- request + GET /exptime +--- response_body_like +^flush_all +status: 200 +exptime: +OK\r +set foo BAR +status: 201 +exptime: 1 +STORED\r +get foo - 0 sec +status: 200 +exptime: +BAR +set foo BAZ +status: 201 +exptime: 2 +STORED\r +get foo - 1 sec +status: 200 +exptime: +BAR +get foo - 2 sec +status: 404 +exptime: +.*?404 Not Found.*$ + diff --git a/t/validate.t b/t/validate.t new file mode 100644 index 0000000..8b0a330 --- /dev/null +++ b/t/validate.t @@ -0,0 +1,161 @@ +use lib 'lib'; +use Test::Nginx::Socket; + +$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211; + +repeat_each(1); + +plan tests => repeat_each() * (2 * blocks()) - 1; + +run_tests(); + +__DATA__ + + +=== TEST 1: test valid sessionid and valid auth token +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +init_by_lua 'service_mappings = {auth="smb", s="flexd"}'; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1595116800'; + echo_subrequest POST '/memc_setup?key=BPSID_MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg*' -b '{"auth_service": "aaa","service_tokens": {"smb": "bbb"}}'; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /validate { + content_by_lua_file '../../build/usr/share/borderpatrol/validate.lua'; +} + +location /auth { # under test + echo_location /setup; + echo_location /validate; + echo $echo_response_status; +} +--- request +GET /auth +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:1595116800:9Wc0CzZKO7Mq5Y2NbTaHrIp/gMg* +--- response_body_like +OK\r +STORED\r +STORED\r +STORED\r +200 + +=== TEST 2: test valid sessionid and but no token stored +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1234567890'; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /validate { + content_by_lua_file '../../build/usr/share/borderpatrol/validate.lua'; +} + +location /auth { # under test + echo_location /setup; + echo_location /validate; + echo $echo_response_status; +} +--- request +GET /auth +--- more_headers +Cookie: border_session=MDEyMzQ1Njc4OTAxMjM0NQ**:Mv+cEjtny9UIrLFYBKFKWQoBvPk* +--- response_body_like +OK\r +STORED\r +STORED\r +.*401 Authorization Required.* + +=== TEST 3: test missing sessionid +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /auth { # under test + content_by_lua_file '../../build/usr/share/borderpatrol/validate.lua'; +} +--- request +GET /auth +--- error_code: 401 + +=== TEST 4: test valid sessionid and valid auth token +--- main_config +--- http_config +lua_package_path "./build/usr/share/borderpatrol/?.lua;./build/usr/share/lua/5.1/?.lua;;"; +lua_package_cpath "./build/usr/lib/lua/5.1/?.so;;"; +--- config +location /memc_setup { + internal; + set $memc_cmd $arg_cmd; + set $memc_key $arg_key; + + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} +location = /setup { + # clear + echo_subrequest GET '/memc_setup?cmd=flush_all'; + echo_subrequest POST '/memc_setup?key=BP_LEASE' -b '1'; + echo_subrequest POST '/memc_setup?key=BPS1' -b 'mysecret:1234567890'; +} +location = /session { + internal; + set $memc_key $arg_id; + memc_pass 127.0.0.1:$TEST_NGINX_MEMCACHED_PORT; +} + +location = /validate { + content_by_lua_file '../../build/usr/share/borderpatrol/validate.lua'; +} + +location /auth { # under test + echo_location /setup; + echo_location /validate; + echo $echo_response_status; +} +--- request +GET /auth +--- more_headers +Cookie: border_session==MTExMTExMTExMTExMTExMQ**:Mv+cEjtny9UIrLFYBKFKWQoBvPk* +--- response_body_like +OK\r +STORED\r +STORED\r +.*401 Authorization Required.*