Initial open source commit

This commit is contained in:
R. Tyler Croy 2014-05-30 16:15:20 -07:00
commit eaa5cfef6f
45 changed files with 3842 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
tmp
.rvmrc
Gemfile.lock
logs
build
t/servroot

18
.gitmodules vendored Normal file
View File

@ -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

8
Gemfile Normal file
View File

@ -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'

22
LICENSE.txt Normal file
View File

@ -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.

149
Makefile Normal file
View File

@ -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}/

121
README.md Normal file
View File

@ -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

1
config Normal file
View File

@ -0,0 +1 @@
have=NDK_HTTP . auto/have

View File

@ -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.
*/

View File

@ -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 <jerome at loyet dot net>
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.

View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,315 @@
/*
* Copyright (C) 2010 Jerome Loyet (jerome at loyet dot net)
*/
#include <nginx.h>
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_md5.h>
#include <ngx_sha1.h>
#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);
}

View File

@ -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 <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <ngx_string.h>
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_ */

View File

@ -0,0 +1,691 @@
/*
* Copyright (C) Jerome Loyet <jerome at loyet dot net>
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#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;
}

7
pkg/after-install.sh Normal file
View File

@ -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

9
pkg/borderpatrol.conf Normal file
View File

@ -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;

148
pkg/borderpatrol.init Normal file
View File

@ -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 <sb@nginx.com>
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

16
src/access.lua Normal file
View File

@ -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

91
src/authorize.lua Normal file
View File

@ -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)

View File

@ -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

55
src/health_check.lua Normal file
View File

@ -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([[
<html>
<head>
<title>Border Patrol Health</title>
</head>
<body>
]])
if #errors > 0 then
ngx.print("<h3>Errors</h3><ul>")
for i, v in ipairs(errors) do
ngx.print("<li>" .. v .. "</li>")
end
ngx.print("</ul>")
else
ngx.print("Everything is ok.")
end
ngx.print([[
</body>
</html>
]])
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)

37
src/logout.lua Normal file
View File

@ -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)

25
src/redirect.lua Normal file
View File

@ -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

2
src/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

49
src/service_token.lua Normal file
View File

@ -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)

275
src/sessionid.lua Normal file
View File

@ -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

11
src/ssl/README.md Normal file
View File

@ -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 <a href="https://www.digitalocean.com/community/articles/how-to-create-a-ssl-certificate-on-nginx-for-ubuntu-12-04">here</a>.

15
src/ssl/server.crt Normal file
View File

@ -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-----

15
src/ssl/server.key Normal file
View File

@ -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-----

83
src/validate.lua Normal file
View File

@ -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)

0
t/.gitkeep Normal file
View File

91
t/access.t Normal file
View File

@ -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$

271
t/authorize.t Normal file
View File

@ -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]

69
t/borderpatrol.god Normal file
View File

@ -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

33
t/health.t Normal file
View File

@ -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

77
t/logout.t Normal file
View File

@ -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+)?/$

105
t/redirect.t Normal file
View File

@ -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

166
t/service_token.t Normal file
View File

@ -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

View File

@ -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"}

48
t/services/api_service.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

128
t/session.t Normal file
View File

@ -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:
<html>.*?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:
<html>.*?404 Not Found.*$

161
t/validate.t Normal file
View File

@ -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.*