From 145710760618430fac792a9bed06d423ac8b86c0 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 20 Feb 2019 08:31:41 +0200 Subject: [PATCH 1/5] Cleanup --- ceryx/nginx/conf/ceryx.conf.tmpl | 4 +- ceryx/nginx/conf/nginx.conf.tmpl | 2 +- ceryx/nginx/lualib/ceryx/redis.lua | 47 ++++++++++++++++++++ ceryx/nginx/lualib/ceryx/routes.lua | 47 ++++++++++++++++++++ ceryx/nginx/lualib/ceryx/utils.lua | 35 +++++++++++++++ ceryx/nginx/lualib/router.lua | 68 ++++++++++------------------- ceryx/redis.lua | 36 +++++++++++++++ 7 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 ceryx/nginx/lualib/ceryx/redis.lua create mode 100644 ceryx/nginx/lualib/ceryx/routes.lua create mode 100644 ceryx/nginx/lualib/ceryx/utils.lua create mode 100644 ceryx/redis.lua diff --git a/ceryx/nginx/conf/ceryx.conf.tmpl b/ceryx/nginx/conf/ceryx.conf.tmpl index 5a84d53..626b5e6 100644 --- a/ceryx/nginx/conf/ceryx.conf.tmpl +++ b/ceryx/nginx/conf/ceryx.conf.tmpl @@ -33,7 +33,7 @@ server { } location / { - set $container_url "fallback"; + set $target "fallback"; # Lua files access_by_lua_file lualib/router.lua; @@ -52,7 +52,7 @@ server { proxy_redirect ~^(https://[^:]+):\d+(/.+)$ $2; proxy_redirect / /; - proxy_pass http://$container_url$request_uri; + proxy_pass $target; } error_page 503 /503.html; diff --git a/ceryx/nginx/conf/nginx.conf.tmpl b/ceryx/nginx/conf/nginx.conf.tmpl index 879e483..b64593d 100644 --- a/ceryx/nginx/conf/nginx.conf.tmpl +++ b/ceryx/nginx/conf/nginx.conf.tmpl @@ -27,7 +27,7 @@ http { error_log /dev/stderr {{ default .Env.CERYX_LOG_LEVEL "info" }}; # Lua settings - lua_package_path "$prefix/lualib/?.lua;;"; + lua_package_path "${prefix}lualib/?.lua;;"; lua_shared_dict ceryx 1m; lua_shared_dict auto_ssl 1m; diff --git a/ceryx/nginx/lualib/ceryx/redis.lua b/ceryx/nginx/lualib/ceryx/redis.lua new file mode 100644 index 0000000..c913dbb --- /dev/null +++ b/ceryx/nginx/lualib/ceryx/redis.lua @@ -0,0 +1,47 @@ +local redis = require "resty.redis" + +local exports = {} + +function exports.prefix() + local prefix = os.getenv("CERYX_REDIS_PREFIX") + if not prefix then prefix = "ceryx" end + return prefix +end + +function exports.client() + local prefix = exports.prefix() + + -- Prepare the Redis client + ngx.log(ngx.DEBUG, "Preparing Redis client.") + local red = redis:new() + red:set_timeout(100) -- 100 ms + local redis_host = os.getenv("CERYX_REDIS_HOST") + if not redis_host then redis_host = "127.0.0.1" end + local redis_port = os.getenv("CERYX_REDIS_PORT") + if not redis_port then redis_port = 6379 end + local redis_password = os.getenv("CERYX_REDIS_PASSWORD") + if not redis_password then redis_password = nil end + local res, err = red:connect(redis_host, redis_port) + + -- Return if could not connect to Redis + if not res then + ngx.log(ngx.DEBUG, "Could not prepare Redis client: " .. err) + return ngx.exit(ngx.HTTP_SERVER_ERROR) + end + + ngx.log(ngx.DEBUG, "Redis client prepared.") + + if redis_password then + ngx.log(ngx.DEBUG, "Authenticating with Redis.") + local res, err = red:auth(redis_password) + if not res then + ngx.ERR("Could not authenticate with Redis: ", err) + return ngx.exit(ngx.HTTP_SERVER_ERROR) + end + end + ngx.log(ngx.DEBUG, "Authenticated with Redis.") + + return red +end + +return exports \ No newline at end of file diff --git a/ceryx/nginx/lualib/ceryx/routes.lua b/ceryx/nginx/lualib/ceryx/routes.lua new file mode 100644 index 0000000..44fc8ef --- /dev/null +++ b/ceryx/nginx/lualib/ceryx/routes.lua @@ -0,0 +1,47 @@ +local redis = require "ceryx.redis" +local prefix = redis.prefix() + +local exports + + +function getRouteForSource(source) + local _ + local target + local settings = {} + local cache = ngx.shared.ceryx + + ngx.log(ngx.DEBUG, "Looking for a route for " .. source) + -- Check if key exists in local cache + local cached_value, _ = cache:get(host) + + if cached_value then + ngx.log(ngx.DEBUG, "Cache hit for " .. source .. ".") + target = cached_value + else + ngx.log(ngx.DEBUG, "Cache miss for " .. host .. ".") + + -- Construct Redis key + local key = prefix .. ":routes:" .. host + + -- Try to get target for host + res, err = redisClient:get(key) + if not res or res == ngx.null then + ngx.log(ngx.INFO, "Could not find target for " .. host .. ".") + + -- Construct Redis key for $wildcard + key = prefix .. ":routes:$wildcard" + res, err = redisClient:get(key) + if not res or res == ngx.null then + ngx.log(ngx.INFO, "No $wildcard target configured for fallback. Exiting with Bad Gateway.") + return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) + else + ngx.log(ngx.DEBUG, "Falling back to " .. res .. ".") + end + end + end + + return route +end + + +return exports \ No newline at end of file diff --git a/ceryx/nginx/lualib/ceryx/utils.lua b/ceryx/nginx/lualib/ceryx/utils.lua new file mode 100644 index 0000000..3d728f5 --- /dev/null +++ b/ceryx/nginx/lualib/ceryx/utils.lua @@ -0,0 +1,35 @@ +local exports = {} + +function starts_with(subject, substring) + return subject:sub(1, #substring) == substring + end + + function ends_with(subject, substring) + return subject:sub(-#substring) == substring + end + + function starts_with_protocol(target) + return starts_with(target, "http://") or starts_with(target, "https://") + end + + function has_trailing_slash(target) + return ends_with(target, "/") + end + + function exports.ensure_protocol(target) + if not starts_with_protocol(target) then + return "http://" .. target + end + + return target + end + + function exports.ensure_no_trailing_slash(target) + if has_trailing_slash(target) then + return target:sub(1, -2) + end + + return target + end + + return exports \ No newline at end of file diff --git a/ceryx/nginx/lualib/router.lua b/ceryx/nginx/lualib/router.lua index 7cb3f31..acff641 100644 --- a/ceryx/nginx/lualib/router.lua +++ b/ceryx/nginx/lualib/router.lua @@ -1,58 +1,38 @@ +local redis = require "ceryx.redis" +local utils = require "ceryx.utils" + +local redisClient = redis:client() +local prefix = redis.prefix() + local host = ngx.var.host -local is_not_https = (ngx.var.scheme ~= "https") local cache = ngx.shared.ceryx -function route(source, target, mode) - if mode == "redirect" then - ngx.log(ngx.INFO, "Redirecting request for " .. source .. " to " .. target .. ".") - return ngx.redirect(target, ngx.HTTP_MOVED_PERMANENTLY) - end +local is_not_https = (ngx.var.scheme ~= "https") +local settings_key = prefix .. ":settings:" .. host - ngx.var.container_url = target - ngx.log(ngx.INFO, "Proxying request for " .. source .. " to " .. target .. ".") -end -local prefix = os.getenv("CERYX_REDIS_PREFIX") -if not prefix then prefix = "ceryx" end - --- Prepare the Redis client -ngx.log(ngx.DEBUG, "Preparing Redis client.") -local redis = require "resty.redis" -local red = redis:new() -red:set_timeout(100) -- 100 ms -local redis_host = os.getenv("CERYX_REDIS_HOST") -if not redis_host then redis_host = "127.0.0.1" end -local redis_port = os.getenv("CERYX_REDIS_PORT") -if not redis_port then redis_port = 6379 end -local redis_password = os.getenv("CERYX_REDIS_PASSWORD") -if not redis_password then redis_password = nil end -local res, err = red:connect(redis_host, redis_port) - --- Return if could not connect to Redis -if not res then - ngx.log(ngx.DEBUG, "Could not prepare Redis client: " .. err) - return ngx.exit(ngx.HTTP_SERVER_ERROR) -end +function route(source, target, mode) + ngx.log(ngx.DEBUG, "Received " .. mode .. " routing request from " .. source .. " to " .. target) + + target = utils.ensure_protocol(target) + target = utils.ensure_no_trailing_slash(target) -ngx.log(ngx.DEBUG, "Redis client prepared.") + local full_target = target .. ngx.var.request_uri -if redis_password then - ngx.log(ngx.DEBUG, "Authenticating with Redis.") - local res, err = red:auth(redis_password) - if not res then - ngx.ERR("Could not authenticate with Redis: ", err) - return ngx.exit(ngx.HTTP_SERVER_ERROR) + if mode == "redirect" then + ngx.log(ngx.INFO, "Redirecting request for " .. source .. " to " .. full_target .. ".") + return ngx.redirect(full_target, ngx.HTTP_MOVED_PERMANENTLY) end -end -ngx.log(ngx.DEBUG, "Authenticated with Redis.") -local settings_key = prefix .. ":settings:" .. host + ngx.var.target = full_target + ngx.log(ngx.INFO, "Proxying request for " .. source .. " to " .. full_target .. ".") +end if is_not_https then local enforce_https, flags = cache:get(host .. ":enforce_https") if enforce_https == nil then - local res, flags = red:hget(settings_key, "enforce_https") + local res, flags = redisClient:hget(settings_key, "enforce_https") enforce_https = tonumber(res) cache:set(host .. ":enforce_https", enforce_https, 5) end @@ -63,7 +43,7 @@ if is_not_https then end -- Get routing mode -local mode, mode_flags = red:hget(settings_key, "mode") +local mode, mode_flags = redisClient:hget(settings_key, "mode") -- Check if key exists in local cache res, flags = cache:get(host) @@ -77,13 +57,13 @@ else local key = prefix .. ":routes:" .. host -- Try to get target for host - res, err = red:get(key) + res, err = redisClient:get(key) if not res or res == ngx.null then ngx.log(ngx.INFO, "Could not find target for " .. host .. ".") -- Construct Redis key for $wildcard key = prefix .. ":routes:$wildcard" - res, err = red:get(key) + res, err = redisClient:get(key) if not res or res == ngx.null then ngx.log(ngx.INFO, "No $wildcard target configured for fallback. Exiting with Bad Gateway.") return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) diff --git a/ceryx/redis.lua b/ceryx/redis.lua new file mode 100644 index 0000000..883805e --- /dev/null +++ b/ceryx/redis.lua @@ -0,0 +1,36 @@ +local redis = require "resty.redis" + +function client() + local prefix = os.getenv("CERYX_REDIS_PREFIX") + if not prefix then prefix = "ceryx" end + + -- Prepare the Redis client + ngx.log(ngx.DEBUG, "Preparing Redis client.") + local red = redis:new() + red:set_timeout(100) -- 100 ms + local redis_host = os.getenv("CERYX_REDIS_HOST") + if not redis_host then redis_host = "127.0.0.1" end + local redis_port = os.getenv("CERYX_REDIS_PORT") + if not redis_port then redis_port = 6379 end + local redis_password = os.getenv("CERYX_REDIS_PASSWORD") + if not redis_password then redis_password = nil end + local res, err = red:connect(redis_host, redis_port) + + -- Return if could not connect to Redis + if not res then + ngx.log(ngx.DEBUG, "Could not prepare Redis client: " .. err) + return ngx.exit(ngx.HTTP_SERVER_ERROR) + end + + ngx.log(ngx.DEBUG, "Redis client prepared.") + + if redis_password then + ngx.log(ngx.DEBUG, "Authenticating with Redis.") + local res, err = red:auth(redis_password) + if not res then + ngx.ERR("Could not authenticate with Redis: ", err) + return ngx.exit(ngx.HTTP_SERVER_ERROR) + end + end + ngx.log(ngx.DEBUG, "Authenticated with Redis.") +end \ No newline at end of file From 6a699f1f2129b20f65a98108675d5bf0851dfb17 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 20 Feb 2019 11:33:36 +0200 Subject: [PATCH 2/5] Further code cleanup --- ceryx/nginx/lualib/ceryx/redis.lua | 35 +++++++------ ceryx/nginx/lualib/ceryx/routes.lua | 74 +++++++++++++++++---------- ceryx/nginx/lualib/ceryx/utils.lua | 72 ++++++++++++++------------ ceryx/nginx/lualib/https.lua | 78 ++++++++--------------------- ceryx/nginx/lualib/router.lua | 69 +++++++++++-------------- 5 files changed, 154 insertions(+), 174 deletions(-) diff --git a/ceryx/nginx/lualib/ceryx/redis.lua b/ceryx/nginx/lualib/ceryx/redis.lua index c913dbb..8260601 100644 --- a/ceryx/nginx/lualib/ceryx/redis.lua +++ b/ceryx/nginx/lualib/ceryx/redis.lua @@ -1,27 +1,21 @@ local redis = require "resty.redis" +local utils = require "ceryx.utils" -local exports = {} +local prefix = utils.getenv("CERYX_REDIS_PREFIX", "ceryx") +local host = utils.getenv("CERYX_REDIS_HOST", "127.0.0.1") +local port = utils.getenv("CERYX_REDIS_PORT", 6379) +local password = utils.getenv("CERYX_REDIS_PASSWORD", nil) -function exports.prefix() - local prefix = os.getenv("CERYX_REDIS_PREFIX") - if not prefix then prefix = "ceryx" end - return prefix -end +local exports = {} function exports.client() - local prefix = exports.prefix() - -- Prepare the Redis client ngx.log(ngx.DEBUG, "Preparing Redis client.") + local red = redis:new() red:set_timeout(100) -- 100 ms - local redis_host = os.getenv("CERYX_REDIS_HOST") - if not redis_host then redis_host = "127.0.0.1" end - local redis_port = os.getenv("CERYX_REDIS_PORT") - if not redis_port then redis_port = 6379 end - local redis_password = os.getenv("CERYX_REDIS_PASSWORD") - if not redis_password then redis_password = nil end - local res, err = red:connect(redis_host, redis_port) + + local res, err = red:connect(host, port) -- Return if could not connect to Redis if not res then @@ -31,9 +25,9 @@ function exports.client() ngx.log(ngx.DEBUG, "Redis client prepared.") - if redis_password then + if password then ngx.log(ngx.DEBUG, "Authenticating with Redis.") - local res, err = red:auth(redis_password) + local res, err = red:auth(password) if not res then ngx.ERR("Could not authenticate with Redis: ", err) return ngx.exit(ngx.HTTP_SERVER_ERROR) @@ -44,4 +38,9 @@ function exports.client() return red end -return exports \ No newline at end of file +exports.prefix = prefix +exports.host = host +exports.port = port +exports.password = password + +return exports diff --git a/ceryx/nginx/lualib/ceryx/routes.lua b/ceryx/nginx/lualib/ceryx/routes.lua index 44fc8ef..daffc45 100644 --- a/ceryx/nginx/lualib/ceryx/routes.lua +++ b/ceryx/nginx/lualib/ceryx/routes.lua @@ -1,47 +1,67 @@ local redis = require "ceryx.redis" -local prefix = redis.prefix() -local exports +local exports = {} +function getRouteKeyForSource(source) + return redis.prefix .. ":routes:" .. source +end + +function targetIsInValid(target) + return not target or target == ngx.null +end + +function getTargetForSource(source) + local redisClient = redis:client() + + -- Construct Redis key and then + -- try to get target for host + local key = getRouteKeyForSource(source) + local target, _ = redisClient:get(key) + + if targetIsInValid(target) then + ngx.log(ngx.INFO, "Could not find target for " .. source .. ".") + + -- Construct Redis key for $wildcard + key = getRouteKeyForSource("$wildcard") + target, _ = redisClient:get(wildcardKey) + + if targetIsInValid(target) then + return nil + end + + ngx.log(ngx.DEBUG, "Falling back to " .. target .. ".") + end + + return target +end function getRouteForSource(source) local _ - local target - local settings = {} + local route = {} local cache = ngx.shared.ceryx ngx.log(ngx.DEBUG, "Looking for a route for " .. source) -- Check if key exists in local cache - local cached_value, _ = cache:get(host) + local cached_value, _ = cache:get(source) if cached_value then - ngx.log(ngx.DEBUG, "Cache hit for " .. source .. ".") - target = cached_value + ngx.log(ngx.DEBUG, "Cache hit for " .. source .. ".") + route.target = cached_value else - ngx.log(ngx.DEBUG, "Cache miss for " .. host .. ".") - - -- Construct Redis key - local key = prefix .. ":routes:" .. host - - -- Try to get target for host - res, err = redisClient:get(key) - if not res or res == ngx.null then - ngx.log(ngx.INFO, "Could not find target for " .. host .. ".") - - -- Construct Redis key for $wildcard - key = prefix .. ":routes:$wildcard" - res, err = redisClient:get(key) - if not res or res == ngx.null then - ngx.log(ngx.INFO, "No $wildcard target configured for fallback. Exiting with Bad Gateway.") - return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) - else - ngx.log(ngx.DEBUG, "Falling back to " .. res .. ".") - end + ngx.log(ngx.DEBUG, "Cache miss for " .. source .. ".") + route.target = getTargetForSource(source) + + if targetIsInValid(route.target) then + return nil end + cache:set(host, res, 5) + ngx.log(ngx.DEBUG, "Caching from " .. source .. " to " .. route.target .. " for 5 seconds.") end return route end +exports.getRouteForSource = getRouteForSource +exports.getTargetForSource = getTargetForSource -return exports \ No newline at end of file +return exports diff --git a/ceryx/nginx/lualib/ceryx/utils.lua b/ceryx/nginx/lualib/ceryx/utils.lua index 3d728f5..b1b1b9f 100644 --- a/ceryx/nginx/lualib/ceryx/utils.lua +++ b/ceryx/nginx/lualib/ceryx/utils.lua @@ -2,34 +2,44 @@ local exports = {} function starts_with(subject, substring) return subject:sub(1, #substring) == substring - end - - function ends_with(subject, substring) - return subject:sub(-#substring) == substring - end - - function starts_with_protocol(target) - return starts_with(target, "http://") or starts_with(target, "https://") - end - - function has_trailing_slash(target) - return ends_with(target, "/") - end - - function exports.ensure_protocol(target) - if not starts_with_protocol(target) then - return "http://" .. target - end - - return target - end - - function exports.ensure_no_trailing_slash(target) - if has_trailing_slash(target) then - return target:sub(1, -2) - end - - return target - end - - return exports \ No newline at end of file +end + +function ends_with(subject, substring) + return subject:sub(-(#substring)) == substring +end + +function starts_with_protocol(target) + return starts_with(target, "http://") or starts_with(target, "https://") +end + +function has_trailing_slash(target) + return ends_with(target, "/") +end + +function exports.ensure_protocol(target) + if not starts_with_protocol(target) then + return "http://" .. target + end + + return target +end + +function exports.ensure_no_trailing_slash(target) + if has_trailing_slash(target) then + return target:sub(1, -2) + end + + return target +end + +function exports.getenv(variable, default) + local value = os.getenv(variable) + + if value then + return value + end + + return default +end + +return exports diff --git a/ceryx/nginx/lualib/https.lua b/ceryx/nginx/lualib/https.lua index d7b696d..d17709e 100644 --- a/ceryx/nginx/lualib/https.lua +++ b/ceryx/nginx/lualib/https.lua @@ -1,72 +1,34 @@ auto_ssl = (require "resty.auto-ssl").new() +local redis = require "ceryx.redis" +local routes = require "ceryx.routes" + -- Define a function to determine which SNI domains to automatically handle -- and register new certificates for. Defaults to not allowing any domains, -- so this must be configured. -auto_ssl:set("allow_domain", function(domain) - local host = domain - - -- Check if key exists in local cache - local cache = ngx.shared.ceryx - local res, flags = cache:get(host) - if res then - return true - end - - local redis = require "resty.redis" - local red = redis:new() - red:set_timeout(100) -- 100 ms - local redis_host = os.getenv("CERYX_REDIS_HOST") - if not redis_host then redis_host = "127.0.0.1" end - local redis_port = os.getenv("CERYX_REDIS_PORT") - if not redis_port then redis_port = 6379 end - local redis_password = os.getenv("CERYX_REDIS_PASSWORD") - if not redis_password then redis_password = nil end - - local res, err = red:connect(redis_host, redis_port) - - -- Return if could not connect to Redis - if not res then - ngx.log(ngx.ERR, "Error connecting to redis: " .. (err or "")) - return false - end - - -- Authenticate with Redis if necessary - if redis_password then - local res, err = red:auth(redis_password) - if not res then - ngx.log(ngx.ERR, "Failed to authenticate with redis: ", err) - return false +auto_ssl:set( + "allow_domain", + function(domain) + local host = domain + local target = routes.getTargetForSource(host) + + if target == nil then + return target end - end - - -- Construct Redis key - local prefix = os.getenv("CERYX_REDIS_PREFIX") - if not prefix then prefix = "ceryx" end - local key = prefix .. ":routes:" .. host - -- Try to get target for host - res, err = red:get(key) - if not res or res == ngx.null then - return false + return true end - - -- Save found key to local cache for 5 seconds - cache:set(host, res, 5) - - return true -end) +) -- Set the resty-auto-ssl storage to Redis, using the CERYX_* env variables -local redis_host = os.getenv("CERYX_REDIS_HOST") -if not redis_host then redis_host = "127.0.0.1" end -local redis_port = os.getenv("CERYX_REDIS_PORT") -if not redis_port then redis_port = 6379 end auto_ssl:set("storage_adapter", "resty.auto-ssl.storage_adapters.redis") -auto_ssl:set("redis", { - host = redis_host, - port = redis_port -}) +auto_ssl:set( + "redis", + { + host = redis.host, + port = redis.port + } +) auto_ssl:init() require "resty.core" diff --git a/ceryx/nginx/lualib/router.lua b/ceryx/nginx/lualib/router.lua index acff641..a6ef09c 100644 --- a/ceryx/nginx/lualib/router.lua +++ b/ceryx/nginx/lualib/router.lua @@ -1,31 +1,42 @@ local redis = require "ceryx.redis" +local routes = require "ceryx.routes" local utils = require "ceryx.utils" local redisClient = redis:client() -local prefix = redis.prefix() local host = ngx.var.host local cache = ngx.shared.ceryx local is_not_https = (ngx.var.scheme ~= "https") -local settings_key = prefix .. ":settings:" .. host - - -function route(source, target, mode) - ngx.log(ngx.DEBUG, "Received " .. mode .. " routing request from " .. source .. " to " .. target) +local settings_key = redis.prefix .. ":settings:" .. host +function formatTarget(target) target = utils.ensure_protocol(target) target = utils.ensure_no_trailing_slash(target) - local full_target = target .. ngx.var.request_uri + return target .. ngx.var.request_uri +end + +function redirect(source, target) + ngx.log(ngx.INFO, "Redirecting request for " .. source .. " to " .. target .. ".") + return ngx.redirect(target, ngx.HTTP_MOVED_PERMANENTLY) +end + +function proxy(source, target) + ngx.var.target = target + ngx.log(ngx.INFO, "Proxying request for " .. source .. " to " .. target .. ".") +end + +function routeRequest(source, target, mode) + ngx.log(ngx.DEBUG, "Received " .. mode .. " routing request from " .. source .. " to " .. target) + + target = formatTarget(target) if mode == "redirect" then - ngx.log(ngx.INFO, "Redirecting request for " .. source .. " to " .. full_target .. ".") - return ngx.redirect(full_target, ngx.HTTP_MOVED_PERMANENTLY) + return redirect(source, target) end - ngx.var.target = full_target - ngx.log(ngx.INFO, "Proxying request for " .. source .. " to " .. full_target .. ".") + return proxy(source, target) end if is_not_https then @@ -45,35 +56,13 @@ end -- Get routing mode local mode, mode_flags = redisClient:hget(settings_key, "mode") --- Check if key exists in local cache -res, flags = cache:get(host) -if res then - ngx.log(ngx.DEBUG, "Cache hit for " .. host .. ".") - route(host, res, mode) -else - ngx.log(ngx.DEBUG, "Cache miss for " .. host .. ".") - - -- Construct Redis key - local key = prefix .. ":routes:" .. host - - -- Try to get target for host - res, err = redisClient:get(key) - if not res or res == ngx.null then - ngx.log(ngx.INFO, "Could not find target for " .. host .. ".") - - -- Construct Redis key for $wildcard - key = prefix .. ":routes:$wildcard" - res, err = redisClient:get(key) - if not res or res == ngx.null then - ngx.log(ngx.INFO, "No $wildcard target configured for fallback. Exiting with Bad Gateway.") - return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) - else - ngx.log(ngx.DEBUG, "Falling back to " .. res .. ".") - end - end +ngx.log(ngx.INFO, "HOST " .. host) +local route = routes.getRouteForSource(host) + +if route == nil then + ngx.log(ngx.INFO, "No $wildcard target configured for fallback. Exiting with Bad Gateway.") + return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE) end -- Save found key to local cache for 5 seconds -route(host, res, mode) -cache:set(host, res, 5) -ngx.log(ngx.DEBUG, "Saving route from " .. host .. " to " .. res .. " in local cache for 5 seconds.") +routeRequest(host, route.target, mode) From 8870209874fd1a394e41a6b572ef16f1cc55c080 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Thu, 28 Feb 2019 16:55:28 +0200 Subject: [PATCH 3/5] Ditch BATS tests in favor of pytest. --- .travis.yml | 2 +- Dockerfile.test | 18 ----- ceryx/Dockerfile.test | 18 +++++ ceryx/Pipfile | 17 +++++ ceryx/Pipfile.lock | 137 +++++++++++++++++++++++++++++++++++++ ceryx/tests/routes.bats | 43 ------------ ceryx/tests/test_routes.py | 105 ++++++++++++++++++++++++++++ docker-compose.test.yml | 4 +- 8 files changed, 280 insertions(+), 64 deletions(-) delete mode 100644 Dockerfile.test create mode 100644 ceryx/Dockerfile.test create mode 100644 ceryx/Pipfile create mode 100644 ceryx/Pipfile.lock delete mode 100644 ceryx/tests/routes.bats create mode 100644 ceryx/tests/test_routes.py diff --git a/.travis.yml b/.travis.yml index 180ffc7..517f33c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ install: - pip install --upgrade --ignore-installed docker-compose==${DOCKER_COMPOSE_VERSION} - docker-compose build - - pip install pipenv==11.9 + - pip install pipenv==2018.11.26 - bash -c "cd api && pipenv install --dev --deploy --system" services: diff --git a/Dockerfile.test b/Dockerfile.test deleted file mode 100644 index 81d6114..0000000 --- a/Dockerfile.test +++ /dev/null @@ -1,18 +0,0 @@ -FROM ubuntu:18.04 - -RUN apt-get update && \ - apt-get install -y curl httpie - -ARG bats_version=1.1.0 -RUN curl -sSL https://github.com/bats-core/bats-core/archive/v${bats_version}.tar.gz | tar -xz && \ - ./bats-core-${bats_version}/install.sh /usr/local && \ - rm -rf ./bats-core-${bats_version} - -ENV CERYX_DEBUG true -ENV CERYX_DISABLE_LETS_ENCRYPT true - -WORKDIR /usr/src/app - -COPY . . - -CMD ["bats", "ceryx/tests"] diff --git a/ceryx/Dockerfile.test b/ceryx/Dockerfile.test new file mode 100644 index 0000000..de92b34 --- /dev/null +++ b/ceryx/Dockerfile.test @@ -0,0 +1,18 @@ +FROM python:3.6 + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +RUN pip install pipenv==2018.11.26 + +WORKDIR /usr/src/app + +COPY Pipfile Pipfile.lock ./ +RUN pipenv install --system --dev --deploy + +COPY . ./ + +ENV CERYX_DEBUG true +ENV CERYX_DISABLE_LETS_ENCRYPT true + +CMD ["pytest", "tests/"] \ No newline at end of file diff --git a/ceryx/Pipfile b/ceryx/Pipfile new file mode 100644 index 0000000..12b317e --- /dev/null +++ b/ceryx/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +pytest = "*" +requests = "*" +black = "==18.9b0" + +[requires] +python_version = "3.6" + +[pipenv] +allow_prereleases = true diff --git a/ceryx/Pipfile.lock b/ceryx/Pipfile.lock new file mode 100644 index 0000000..2a96d81 --- /dev/null +++ b/ceryx/Pipfile.lock @@ -0,0 +1,137 @@ +{ + "_meta": { + "hash": { + "sha256": "9b114b9d5064e0882163dd1bcf406f3b55526a07687e182c3f095301d54a17f1" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "black": { + "hashes": [ + "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", + "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5" + ], + "index": "pypi", + "version": "==18.9b0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "markers": "python_version > '2.7'", + "version": "==6.0.0" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pytest": { + "hashes": [ + "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", + "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + } +} diff --git a/ceryx/tests/routes.bats b/ceryx/tests/routes.bats deleted file mode 100644 index fd85a50..0000000 --- a/ceryx/tests/routes.bats +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bats - -@test "503 response when no route exist for the given host" { - status_code=$(curl -s -o /dev/null -I -w "%{http_code}" -H "Host: i-do-not-exist" http://ceryx) - [ $status_code -eq 503 ] -} - -@test "Successful proxying when target exists" { - curl -s -o /dev/null \ - -X POST \ - -H 'Content-Type: application/json' \ - -d '{"source": "api-route", "target": "api:5555"}' \ - http://api:5555/api/routes/ - - upstream_status_code=$(curl -s -o /dev/null -I -w "%{http_code}" http://api:5555/api/routes/) - ceryx_status_code=$(curl -s -o /dev/null -I -w "%{http_code}" -H "Host: api-route" http://ceryx/api/routes/) - - [ $ceryx_status_code -eq $upstream_status_code ] -} - -@test "301 response when enforce_https=true" { - curl -s -o /dev/null \ - -X POST \ - -H 'Content-Type: application/json' \ - -d '{"source": "enforced-https-route", "target": "somewhere", "settings":{"enforce_https": true}}' \ - http://api:5555/api/routes/ - - status_code=$(curl -s -o /dev/null -I -w "%{http_code}" -H "Host: enforced-https-route" http://ceryx/) - - [ $status_code -eq 301 ] -} - -@test "301 response when mode=redirect" { - curl -s -o /dev/null \ - -X POST \ - -H 'Content-Type: application/json' \ - -d '{"source": "redirected-route", "target": "redirection-target", "settings":{"mode": "redirect"}}' \ - http://api:5555/api/routes/ - - status_code=$(curl -s -o /dev/null -I -w "%{http_code}" -H "Host: redirected-route" http://ceryx/) - - [ $status_code -eq 301 ] -} diff --git a/ceryx/tests/test_routes.py b/ceryx/tests/test_routes.py new file mode 100644 index 0000000..e2c6adf --- /dev/null +++ b/ceryx/tests/test_routes.py @@ -0,0 +1,105 @@ +import os + +import requests + +CERYX_API_URL = os.getenv("CERYX_API_URL", "http://api:5555") +CERYX_API_ROUTES_ROOT = os.path.join(CERYX_API_URL, "api/routes") + +CERYX_HOST = "http://ceryx" + + +def test_no_route(): + """ + Ceryx should send a `503` response when receiving a request with a `Host` + header that has not been registered for routing. + """ + response = requests.get( + CERYX_HOST, headers={"Host": "i-do-not-exist.ceryx.test"} + ) + assert response.status_code == 503 + + +def test_proxy(): + """ + Ceryx should successfully proxy the upstream request to the client, for a + registered route. + """ + api_upstream_host = "api" + ceryx_route_source = "api.ceryx.test" + ceryx_route_target = f"http://{api_upstream_host}:5555/api/routes" + + # Register the local Ceryx API as a route + register_api_response = requests.post( + CERYX_API_ROUTES_ROOT, + json={"source": ceryx_route_source, "target": ceryx_route_target}, + ) + + upstream_response = requests.get( + ceryx_route_target, headers={"Host": api_upstream_host} + ) + ceryx_response = requests.get( + f"{CERYX_HOST}", headers={"Host": ceryx_route_source} + ) + + assert upstream_response.status_code == ceryx_response.status_code + assert upstream_response.content == ceryx_response.content + + +def test_redirect(): + """ + Ceryx should respond with 301 status and the appropriate `Location` header + for redirected routes. + """ + api_upstream_host = "api" + ceryx_route_target = "http://api:5555/api/routes" + ceryx_route_source = "redirected-api.ceryx.test" + + # Register the local Ceryx API as a route + register_api_response = requests.post( + CERYX_API_ROUTES_ROOT, + json={ + "source": ceryx_route_source, + "target": ceryx_route_target, + "settings": {"mode": "redirect"}, + }, + ) + + original_url = f"{CERYX_HOST}/some/path/?some=args&more=args" + target_url = f"{ceryx_route_target}/some/path/?some=args&more=args" + + ceryx_response = requests.get( + original_url, headers={"Host": ceryx_route_source}, allow_redirects=False, + ) + + assert ceryx_response.status_code == 301 + assert ceryx_response.headers["Location"] == target_url + + +def test_enforce_https(): + """ + Ceryx should respond with 301 status and the appropriate `Location` header + for routes with HTTPS enforced. + """ + api_upstream_host = "api" + api_upstream_target = "http://api:5555/" + ceryx_route_source = "secure-api.ceryx.test" + + # Register the local Ceryx API as a route + register_api_response = requests.post( + CERYX_API_ROUTES_ROOT, + json={ + "source": ceryx_route_source, + "target": api_upstream_target, + "settings": {"enforce_https": True}, + }, + ) + + + original_url = f"{CERYX_HOST}/some/path/?some=args&more=args" + secure_url = f"https://{ceryx_route_source}/some/path/?some=args&more=args" + ceryx_response = requests.get( + original_url, headers={"Host": ceryx_route_source}, allow_redirects=False, + ) + + assert ceryx_response.status_code == 301 + assert ceryx_response.headers["Location"] == secure_url diff --git a/docker-compose.test.yml b/docker-compose.test.yml index e72198b..60762b8 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -3,12 +3,12 @@ version: '3.5' services: test: build: - context: . + context: ./ceryx dockerfile: Dockerfile.test environment: CERYX_API_URL: "http://api:${CERYX_API_PORT:-5555}" volumes: - - .:/usr/src/app + - ./ceryx:/usr/src/app depends_on: - ceryx - api From 3d03a7109264b5cf78c945d5731e3f0bd567b81a Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Thu, 28 Feb 2019 17:13:07 +0200 Subject: [PATCH 4/5] Enforce target protocol in the API. --- api/app.py | 7 ++-- api/ceryx/db.py | 15 ++++++++ api/tests.py | 92 ++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/api/app.py b/api/app.py index 3235e6b..6ca8e9a 100644 --- a/api/app.py +++ b/api/app.py @@ -15,13 +15,12 @@ def list_routes() -> typing.List[types.Route]: def create_route(route: types.Route) -> types.Route: - ROUTER.insert(**route) - return http.JSONResponse(route, status_code=201) + created_route = ROUTER.insert(**route) + return http.JSONResponse(created_route, status_code=201) def update_route(source: str, route: types.RouteWithoutSource) -> types.Route: - ROUTER.insert(source, **route) - updated_route = dict(source=source, **route) + updated_route = ROUTER.insert(source, **route) return types.Route(updated_route) diff --git a/api/ceryx/db.py b/api/ceryx/db.py index 21d390a..5199eba 100644 --- a/api/ceryx/db.py +++ b/api/ceryx/db.py @@ -1,11 +1,16 @@ """ Simple Redis client, implemented the data logic of Ceryx. """ +import re + import redis from ceryx import settings +STARTS_WITH_PROTOCOL = r'^https?://' + + def _str(subject): return subject.decode('utf-8') if type(subject) == bytes else str(bytes) @@ -163,9 +168,19 @@ def insert(self, source, target, settings): """ Inserts a new source/target host entry in to the database. """ + target = ( + target if re.match(STARTS_WITH_PROTOCOL, target) + else f'http://{target}' + ) route_key = self._prefixed_route_key(source) self.client.set(route_key, target) self._set_settings_for_source(source, settings) + route = { + 'source': source, + 'target': target, + 'settings': settings, + } + return route def delete(self, source): """ diff --git a/api/tests.py b/api/tests.py index 97abfd0..8fc01f3 100644 --- a/api/tests.py +++ b/api/tests.py @@ -20,17 +20,17 @@ def test_list_routes(self): self.assertEqual(response.status_code, 200) self.assertEqual(type(response.json()), list) - def test_create_route(self): + def test_create_route_without_protocol(self): """ Assert that creating a route, will result in the appropriate route. """ - route_data = { + request_body = { 'source': 'test.dev', 'target': 'localhost:11235', } - expected_response = { + response_body = { 'source': 'test.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', 'settings': { 'enforce_https': False, 'mode': 'proxy', @@ -38,26 +38,78 @@ def test_create_route(self): } # Create a route and assert valid data in response - response = self.client.post('/api/routes', json=route_data) + response = self.client.post('/api/routes', json=request_body) self.assertEqual(response.status_code, 201) - self.assertDictEqual(response.json(), expected_response) + self.assertDictEqual(response.json(), response_body) # Also get the route and assert valid data response = self.client.get('/api/routes/test.dev') - self.assertDictEqual(response.json(), expected_response) + self.assertDictEqual(response.json(), response_body) + + def test_create_route_with_http_protocol(self): + """ + Assert that creating a route, will result in the appropriate route. + """ + request_body = { + 'source': 'test.dev', + 'target': 'http://localhost:11235', + } + response_body = { + 'source': 'test.dev', + 'target': 'http://localhost:11235', + 'settings': { + 'enforce_https': False, + 'mode': 'proxy', + } + } + + # Create a route and assert valid data in response + response = self.client.post('/api/routes', json=request_body) + self.assertEqual(response.status_code, 201) + self.assertDictEqual(response.json(), response_body) + + # Also get the route and assert valid data + response = self.client.get('/api/routes/test.dev') + self.assertDictEqual(response.json(), response_body) + + def test_create_route_with_https_protocol(self): + """ + Assert that creating a route, will result in the appropriate route. + """ + request_body = { + 'source': 'test.dev', + 'target': 'https://localhost:11235', + } + response_body = { + 'source': 'test.dev', + 'target': 'https://localhost:11235', + 'settings': { + 'enforce_https': False, + 'mode': 'proxy', + } + } + + # Create a route and assert valid data in response + response = self.client.post('/api/routes', json=request_body) + self.assertEqual(response.status_code, 201) + self.assertDictEqual(response.json(), response_body) + + # Also get the route and assert valid data + response = self.client.get('/api/routes/test.dev') + self.assertDictEqual(response.json(), response_body) def test_enforce_https(self): """ Assert that creating a route with the `enforce_https` settings returns the expected results """ - route_without_enforce_https = { + route_without_enforce_https_request_body = { 'source': 'test-no-enforce-https.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', } route_enforce_https_true = { 'source': 'test-enforce-https-true.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', 'settings': { 'enforce_https': True, 'mode': 'proxy', @@ -65,30 +117,30 @@ def test_enforce_https(self): } route_enforce_https_false = { 'source': 'test-enforce-https-false.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', 'settings': { 'enforce_https': False, 'mode': 'proxy', }, } - expected_response_without_enforce_https = { + route_without_enforce_https_response_body = { 'source': 'test-no-enforce-https.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', 'settings': { 'enforce_https': False, 'mode': 'proxy', }, } - response = self.client.post('/api/routes', json=route_without_enforce_https) + response = self.client.post('/api/routes', json=route_without_enforce_https_request_body) self.assertEqual(response.status_code, 201) self.assertDictEqual( - response.json(), expected_response_without_enforce_https, + response.json(), route_without_enforce_https_response_body, ) response = self.client.get('/api/routes/test-no-enforce-https.dev') self.assertDictEqual( - response.json(), expected_response_without_enforce_https, + response.json(), route_without_enforce_https_response_body, ) response = self.client.post('/api/routes', json=route_enforce_https_true) @@ -120,11 +172,11 @@ def test_mode(self): """ route_without_mode = { 'source': 'www.my-website.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', } route_mode_proxy = { 'source': 'www.my-website.dev', - 'target': 'localhost:11235', + 'target': 'http://localhost:11235', 'settings': { 'enforce_https': False, 'mode': 'proxy', @@ -132,7 +184,7 @@ def test_mode(self): } route_mode_redirect = { 'source': 'my-website.dev', - 'target': 'www.my-website.dev', + 'target': 'http://www.my-website.dev', 'settings': { 'enforce_https': False, 'mode': 'redirect', @@ -178,7 +230,7 @@ def test_delete_route(self): """ route_data = { 'source': 'test.dev', - 'target': 'localhost:11235' + 'target': 'http://localhost:11235' } # Create a route From b81454ee33b6435c34d726f330ad392e914207e6 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Thu, 28 Feb 2019 17:17:32 +0200 Subject: [PATCH 5/5] Format --- api/Pipfile | 1 + api/Pipfile.lock | 105 ++++++++++++------ api/app.py | 73 ++++++++----- api/ceryx/db.py | 68 ++++++------ api/ceryx/settings.py | 18 ++-- api/ceryx/types.py | 12 +-- api/tests.py | 241 ++++++++++++++++-------------------------- 7 files changed, 253 insertions(+), 265 deletions(-) diff --git a/api/Pipfile b/api/Pipfile index e67a525..18413a0 100644 --- a/api/Pipfile +++ b/api/Pipfile @@ -10,6 +10,7 @@ requests = ">=2.21.0" [dev-packages] nose = "*" +black = "==18.9b0" [requires] python_version = "3.6" diff --git a/api/Pipfile.lock b/api/Pipfile.lock index d148a42..dfb63cb 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "55c00a87a93e136abed2ad708ac76135c7548321b1192a5209ba651d7e2d195e" + "sha256": "5c056fe85a233763e10d9f421623a691326fe7fab0d634a792f47bb69b6f5505" }, "pipfile-spec": 6, "requires": { @@ -53,44 +53,44 @@ }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" - ], - "version": "==1.1.0" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" }, "redis": { "hashes": [ - "sha256:2100750629beff143b6a200a2ea8e719fcf26420adabb81402895e144c5083cf", - "sha256:8e0bdd2de02e829b6225b25646f9fb9daffea99a252610d040409a6738541f0a" + "sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54", + "sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775" ], "index": "pypi", - "version": "==3.0.1" + "version": "==3.2.0" }, "requests": { "hashes": [ @@ -123,6 +123,35 @@ } }, "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "attrs": { + "hashes": [ + "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", + "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + ], + "version": "==18.2.0" + }, + "black": { + "hashes": [ + "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", + "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5" + ], + "index": "pypi", + "version": "==18.9b0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, "nose": { "hashes": [ "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", @@ -131,6 +160,14 @@ ], "index": "pypi", "version": "==1.3.7" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", + "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3" + ], + "version": "==0.10.0" } } } diff --git a/api/app.py b/api/app.py index 6ca8e9a..97313da 100644 --- a/api/app.py +++ b/api/app.py @@ -27,55 +27,74 @@ def update_route(source: str, route: types.RouteWithoutSource) -> types.Route: def get_route(source: str) -> types.Route: try: resource = { - 'source': source, - 'target': ROUTER.lookup(source), - 'settings': ROUTER.lookup_settings(source), + "source": source, + "target": ROUTER.lookup(source), + "settings": ROUTER.lookup_settings(source), } return resource except RedisRouter.LookupNotFound: return http.JSONResponse( - {'message': f'Route with source {source} doesn\'t exist'}, - status_code=404, + {"message": f"Route with source {source} doesn't exist"}, status_code=404 ) def delete_route(source: str) -> types.Route: try: route = { - 'source': source, - 'target': ROUTER.lookup(source), - 'settings': ROUTER.lookup_settings(source), + "source": source, + "target": ROUTER.lookup(source), + "settings": ROUTER.lookup_settings(source), } ROUTER.delete(source) - return http.JSONResponse( - types.Route(route), - status_code=204, - ) + return http.JSONResponse(types.Route(route), status_code=204) except RedisRouter.LookupNotFound: return http.JSONResponse( - {'message': f'Route with source {source} doesn\'t exist'}, - status_code=404, + {"message": f"Route with source {source} doesn't exist"}, status_code=404 ) routes = [ - Route('/api/routes', method='GET', handler=list_routes), - Route('/api/routes', method='POST', handler=create_route), - Route('/api/routes/{source}', method='GET', handler=get_route), - Route('/api/routes/{source}', method='PUT', handler=update_route), - Route('/api/routes/{source}', method='DELETE', handler=delete_route), - + Route("/api/routes", method="GET", handler=list_routes), + Route("/api/routes", method="POST", handler=create_route), + Route("/api/routes/{source}", method="GET", handler=get_route), + Route("/api/routes/{source}", method="PUT", handler=update_route), + Route("/api/routes/{source}", method="DELETE", handler=delete_route), # Allow trailing slashes as well (GitHub style) - Route('/api/routes/', method='GET', handler=list_routes, name='list_routes_trailing_slash'), - Route('/api/routes/', method='POST', handler=create_route, name='create_route_trailing_slash'), - Route('/api/routes/{source}/', method='GET', handler=get_route, name='get_route_trailing_slash'), - Route('/api/routes/{source}/', method='PUT', handler=update_route, name='update_route_trailing_slash'), - Route('/api/routes/{source}/', method='DELETE', handler=delete_route, name='delete_route_trailing_slash'), + Route( + "/api/routes/", + method="GET", + handler=list_routes, + name="list_routes_trailing_slash", + ), + Route( + "/api/routes/", + method="POST", + handler=create_route, + name="create_route_trailing_slash", + ), + Route( + "/api/routes/{source}/", + method="GET", + handler=get_route, + name="get_route_trailing_slash", + ), + Route( + "/api/routes/{source}/", + method="PUT", + handler=update_route, + name="update_route_trailing_slash", + ), + Route( + "/api/routes/{source}/", + method="DELETE", + handler=delete_route, + name="delete_route_trailing_slash", + ), ] app = App(routes=routes) -if __name__ == '__main__': +if __name__ == "__main__": from ceryx import settings app.serve( @@ -83,4 +102,4 @@ def delete_route(source: str) -> types.Route: settings.API_BIND_PORT, use_debugger=settings.DEBUG, use_reloader=settings.DEBUG, - ) \ No newline at end of file + ) diff --git a/api/ceryx/db.py b/api/ceryx/db.py index 5199eba..679f070 100644 --- a/api/ceryx/db.py +++ b/api/ceryx/db.py @@ -8,11 +8,11 @@ from ceryx import settings -STARTS_WITH_PROTOCOL = r'^https?://' +STARTS_WITH_PROTOCOL = r"^https?://" def _str(subject): - return subject.decode('utf-8') if type(subject) == bytes else str(bytes) + return subject.decode("utf-8") if type(subject) == bytes else str(bytes) def encode_settings(settings): @@ -20,8 +20,8 @@ def encode_settings(settings): Encode and sanitize settings in order to be written to Redis. """ encoded_settings = { - 'enforce_https': str(int(settings.get('enforce_https', False))), - 'mode': settings.get('mode', 'proxy'), + "enforce_https": str(int(settings.get("enforce_https", False))), + "mode": settings.get("mode", "proxy"), } return encoded_settings @@ -34,12 +34,10 @@ def decode_settings(settings): # If any of the keys or values of the provided settings are bytes, then # convert them to strings. - _settings = { - _str(k): _str(v) for k, v in settings.items() - } + _settings = {_str(k): _str(v) for k, v in settings.items()} decoded = { - 'enforce_https': bool(int(_settings.get('enforce_https', '0'))), - 'mode': _settings.get('mode', 'proxy'), + "enforce_https": bool(int(_settings.get("enforce_https", "0"))), + "mode": _settings.get("mode", "proxy"), } return decoded @@ -49,6 +47,7 @@ class RedisRouter(object): """ Router using a redis backend, in order to route incoming requests. """ + class LookupNotFound(Exception): """ Exception raised when a lookup for a specific host was not found. @@ -57,7 +56,7 @@ class LookupNotFound(Exception): def __init__(self, message, errors=None): Exception.__init__(self, message) if errors is None: - self.errors = {'message': message} + self.errors = {"message": message} else: self.errors = errors @@ -67,12 +66,16 @@ def from_config(path=None): Returns a RedisRouter, using the default configuration from Ceryx settings. """ - return RedisRouter(settings.REDIS_HOST, settings.REDIS_PORT, - settings.REDIS_PASSWORD, 0, settings.REDIS_PREFIX) + return RedisRouter( + settings.REDIS_HOST, + settings.REDIS_PORT, + settings.REDIS_PASSWORD, + 0, + settings.REDIS_PREFIX, + ) def __init__(self, host, port, password, db, prefix): - self.client = redis.StrictRedis( - host=host, port=port, password=password, db=db) + self.client = redis.StrictRedis(host=host, port=port, password=password, db=db) self.prefix = prefix def _prefixed_route_key(self, source): @@ -80,9 +83,9 @@ def _prefixed_route_key(self, source): Returns the prefixed key, if prefix has been defined, for the given route. """ - prefixed_key = 'routes:%s' + prefixed_key = "routes:%s" if self.prefix is not None: - prefixed_key = self.prefix + ':routes:%s' + prefixed_key = self.prefix + ":routes:%s" prefixed_key = prefixed_key % source return prefixed_key @@ -91,16 +94,16 @@ def _prefixed_settings_key(self, source): Returns the prefixed key, if prefix has been defined, for the given source's setting. """ - prefixed_key = 'settings:%s' + prefixed_key = "settings:%s" if self.prefix is not None: - prefixed_key = self.prefix + ':settings:%s' + prefixed_key = self.prefix + ":settings:%s" prefixed_key = prefixed_key % source return prefixed_key - + def _delete_settings_for_source(self, source): settings_key = self._prefixed_settings_key(source) self.client.delete(settings_key) - + def _set_settings_for_source(self, source, settings): settings_key = self._prefixed_settings_key(source) @@ -120,9 +123,7 @@ def lookup(self, host, silent=False): target_host = self.client.get(lookup_host) if target_host is None and not silent: - raise RedisRouter.LookupNotFound( - 'Given host does not match with any route' - ) + raise RedisRouter.LookupNotFound("Given host does not match with any route") else: return _str(target_host) @@ -141,13 +142,13 @@ def lookup_hosts(self, pattern): all hosts are returned. """ if not pattern: - pattern = '*' + pattern = "*" lookup_pattern = self._prefixed_route_key(pattern) keys = self.client.keys(lookup_pattern) - filtered_keys = [key[len(lookup_pattern) - len(pattern):] for key in keys] + filtered_keys = [key[len(lookup_pattern) - len(pattern) :] for key in keys] return [_str(key) for key in filtered_keys] - def lookup_routes(self, pattern='*'): + def lookup_routes(self, pattern="*"): """ Fetches routes with host that matches the given pattern. If no pattern is given, all routes are returned. @@ -157,9 +158,9 @@ def lookup_routes(self, pattern='*'): for host in hosts: routes.append( { - 'source': host, - 'target': self.lookup(host, silent=True), - 'settings': self.lookup_settings(host), + "source": host, + "target": self.lookup(host, silent=True), + "settings": self.lookup_settings(host), } ) return routes @@ -169,17 +170,12 @@ def insert(self, source, target, settings): Inserts a new source/target host entry in to the database. """ target = ( - target if re.match(STARTS_WITH_PROTOCOL, target) - else f'http://{target}' + target if re.match(STARTS_WITH_PROTOCOL, target) else f"http://{target}" ) route_key = self._prefixed_route_key(source) self.client.set(route_key, target) self._set_settings_for_source(source, settings) - route = { - 'source': source, - 'target': target, - 'settings': settings, - } + route = {"source": source, "target": target, "settings": settings} return route def delete(self, source): diff --git a/api/ceryx/settings.py b/api/ceryx/settings.py index 2bc2863..c07fc07 100644 --- a/api/ceryx/settings.py +++ b/api/ceryx/settings.py @@ -6,17 +6,17 @@ DEBUG = True -if os.getenv('CERYX_DEBUG', '').lower() in ['0', 'no', 'false']: +if os.getenv("CERYX_DEBUG", "").lower() in ["0", "no", "false"]: DEBUG = False -API_BIND_HOST = os.getenv('CERYX_API_HOST', '127.0.0.1') -API_BIND_PORT = int(os.getenv('CERYX_API_PORT', 5555)) -SECRET_KEY = os.getenv('CERYX_SECRET_KEY') +API_BIND_HOST = os.getenv("CERYX_API_HOST", "127.0.0.1") +API_BIND_PORT = int(os.getenv("CERYX_API_PORT", 5555)) +SECRET_KEY = os.getenv("CERYX_SECRET_KEY") if SECRET_KEY: - with open(SECRET_KEY, 'r') as f: + with open(SECRET_KEY, "r") as f: SECRET_KEY = f.read() -REDIS_HOST = os.getenv('CERYX_REDIS_HOST', '127.0.0.1') -REDIS_PORT = int(os.getenv('CERYX_REDIS_PORT', 6379)) -REDIS_PASSWORD = os.getenv('CERYX_REDIS_PASSWORD', None) -REDIS_PREFIX = os.getenv('CERYX_REDIS_PREFIX', 'ceryx') +REDIS_HOST = os.getenv("CERYX_REDIS_HOST", "127.0.0.1") +REDIS_PORT = int(os.getenv("CERYX_REDIS_PORT", 6379)) +REDIS_PASSWORD = os.getenv("CERYX_REDIS_PASSWORD", None) +REDIS_PREFIX = os.getenv("CERYX_REDIS_PREFIX", "ceryx") diff --git a/api/ceryx/types.py b/api/ceryx/types.py index 1f26733..357f2a0 100644 --- a/api/ceryx/types.py +++ b/api/ceryx/types.py @@ -3,16 +3,10 @@ SETTINGS_VALIDATOR = validators.Object( properties={ - 'enforce_https': validators.Boolean(default=False), - 'mode': validators.String( - default='proxy', - enum=['proxy', 'redirect'], - ), - }, - default={ - 'enforce_https': False, - 'mode': 'proxy', + "enforce_https": validators.Boolean(default=False), + "mode": validators.String(default="proxy", enum=["proxy", "redirect"]), }, + default={"enforce_https": False, "mode": "proxy"}, ) diff --git a/api/tests.py b/api/tests.py index 8fc01f3..c828d18 100644 --- a/api/tests.py +++ b/api/tests.py @@ -16,7 +16,7 @@ def test_list_routes(self): """ Assert that listing routes will return a JSON list. """ - response = self.client.get('/api/routes') + response = self.client.get("/api/routes") self.assertEqual(response.status_code, 200) self.assertEqual(type(response.json()), list) @@ -24,78 +24,60 @@ def test_create_route_without_protocol(self): """ Assert that creating a route, will result in the appropriate route. """ - request_body = { - 'source': 'test.dev', - 'target': 'localhost:11235', - } + request_body = {"source": "test.dev", "target": "localhost:11235"} response_body = { - 'source': 'test.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - } + "source": "test.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } - + # Create a route and assert valid data in response - response = self.client.post('/api/routes', json=request_body) + response = self.client.post("/api/routes", json=request_body) self.assertEqual(response.status_code, 201) self.assertDictEqual(response.json(), response_body) - + # Also get the route and assert valid data - response = self.client.get('/api/routes/test.dev') + response = self.client.get("/api/routes/test.dev") self.assertDictEqual(response.json(), response_body) def test_create_route_with_http_protocol(self): """ Assert that creating a route, will result in the appropriate route. """ - request_body = { - 'source': 'test.dev', - 'target': 'http://localhost:11235', - } + request_body = {"source": "test.dev", "target": "http://localhost:11235"} response_body = { - 'source': 'test.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - } + "source": "test.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } - + # Create a route and assert valid data in response - response = self.client.post('/api/routes', json=request_body) + response = self.client.post("/api/routes", json=request_body) self.assertEqual(response.status_code, 201) self.assertDictEqual(response.json(), response_body) - + # Also get the route and assert valid data - response = self.client.get('/api/routes/test.dev') + response = self.client.get("/api/routes/test.dev") self.assertDictEqual(response.json(), response_body) def test_create_route_with_https_protocol(self): """ Assert that creating a route, will result in the appropriate route. """ - request_body = { - 'source': 'test.dev', - 'target': 'https://localhost:11235', - } + request_body = {"source": "test.dev", "target": "https://localhost:11235"} response_body = { - 'source': 'test.dev', - 'target': 'https://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - } + "source": "test.dev", + "target": "https://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } - + # Create a route and assert valid data in response - response = self.client.post('/api/routes', json=request_body) + response = self.client.post("/api/routes", json=request_body) self.assertEqual(response.status_code, 201) self.assertDictEqual(response.json(), response_body) - + # Also get the route and assert valid data - response = self.client.get('/api/routes/test.dev') + response = self.client.get("/api/routes/test.dev") self.assertDictEqual(response.json(), response_body) def test_enforce_https(self): @@ -104,66 +86,47 @@ def test_enforce_https(self): the expected results """ route_without_enforce_https_request_body = { - 'source': 'test-no-enforce-https.dev', - 'target': 'http://localhost:11235', + "source": "test-no-enforce-https.dev", + "target": "http://localhost:11235", } route_enforce_https_true = { - 'source': 'test-enforce-https-true.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': True, - 'mode': 'proxy', - }, + "source": "test-enforce-https-true.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": True, "mode": "proxy"}, } route_enforce_https_false = { - 'source': 'test-enforce-https-false.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - }, + "source": "test-enforce-https-false.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } route_without_enforce_https_response_body = { - 'source': 'test-no-enforce-https.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - }, + "source": "test-no-enforce-https.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } - - response = self.client.post('/api/routes', json=route_without_enforce_https_request_body) - self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_without_enforce_https_response_body, - ) - - response = self.client.get('/api/routes/test-no-enforce-https.dev') - self.assertDictEqual( - response.json(), route_without_enforce_https_response_body, + + response = self.client.post( + "/api/routes", json=route_without_enforce_https_request_body ) - - response = self.client.post('/api/routes', json=route_enforce_https_true) self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_enforce_https_true, - ) - - response = self.client.get('/api/routes/test-enforce-https-true.dev') - self.assertDictEqual( - response.json(), route_enforce_https_true, - ) - - response = self.client.post('/api/routes', json=route_enforce_https_false) + self.assertDictEqual(response.json(), route_without_enforce_https_response_body) + + response = self.client.get("/api/routes/test-no-enforce-https.dev") + self.assertDictEqual(response.json(), route_without_enforce_https_response_body) + + response = self.client.post("/api/routes", json=route_enforce_https_true) self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_enforce_https_false, - ) - - response = self.client.get('/api/routes/test-enforce-https-false.dev') - self.assertDictEqual( - response.json(), route_enforce_https_false, - ) + self.assertDictEqual(response.json(), route_enforce_https_true) + + response = self.client.get("/api/routes/test-enforce-https-true.dev") + self.assertDictEqual(response.json(), route_enforce_https_true) + + response = self.client.post("/api/routes", json=route_enforce_https_false) + self.assertEqual(response.status_code, 201) + self.assertDictEqual(response.json(), route_enforce_https_false) + + response = self.client.get("/api/routes/test-enforce-https-false.dev") + self.assertDictEqual(response.json(), route_enforce_https_false) def test_mode(self): """ @@ -171,80 +134,58 @@ def test_mode(self): the expected results. """ route_without_mode = { - 'source': 'www.my-website.dev', - 'target': 'http://localhost:11235', + "source": "www.my-website.dev", + "target": "http://localhost:11235", } route_mode_proxy = { - 'source': 'www.my-website.dev', - 'target': 'http://localhost:11235', - 'settings': { - 'enforce_https': False, - 'mode': 'proxy', - }, + "source": "www.my-website.dev", + "target": "http://localhost:11235", + "settings": {"enforce_https": False, "mode": "proxy"}, } route_mode_redirect = { - 'source': 'my-website.dev', - 'target': 'http://www.my-website.dev', - 'settings': { - 'enforce_https': False, - 'mode': 'redirect', - }, + "source": "my-website.dev", + "target": "http://www.my-website.dev", + "settings": {"enforce_https": False, "mode": "redirect"}, } - - response = self.client.post('/api/routes', json=route_without_mode) + + response = self.client.post("/api/routes", json=route_without_mode) self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_mode_proxy, - ) - - response = self.client.get('/api/routes/www.my-website.dev') - self.assertDictEqual( - response.json(), route_mode_proxy, - ) - - response = self.client.post('/api/routes', json=route_mode_proxy) + self.assertDictEqual(response.json(), route_mode_proxy) + + response = self.client.get("/api/routes/www.my-website.dev") + self.assertDictEqual(response.json(), route_mode_proxy) + + response = self.client.post("/api/routes", json=route_mode_proxy) self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_mode_proxy, - ) - - response = self.client.get('/api/routes/www.my-website.dev') - self.assertDictEqual( - response.json(), route_mode_proxy, - ) - - response = self.client.post('/api/routes', json=route_mode_redirect) + self.assertDictEqual(response.json(), route_mode_proxy) + + response = self.client.get("/api/routes/www.my-website.dev") + self.assertDictEqual(response.json(), route_mode_proxy) + + response = self.client.post("/api/routes", json=route_mode_redirect) self.assertEqual(response.status_code, 201) - self.assertDictEqual( - response.json(), route_mode_redirect, - ) - - response = self.client.get('/api/routes/my-website.dev') - self.assertDictEqual( - response.json(), route_mode_redirect, - ) + self.assertDictEqual(response.json(), route_mode_redirect) + + response = self.client.get("/api/routes/my-website.dev") + self.assertDictEqual(response.json(), route_mode_redirect) def test_delete_route(self): """ Assert that deleting a route, will actually delete it. """ - route_data = { - 'source': 'test.dev', - 'target': 'http://localhost:11235' - } - + route_data = {"source": "test.dev", "target": "http://localhost:11235"} + # Create a route - response = self.client.post('/api/routes', json=route_data) - + response = self.client.post("/api/routes", json=route_data) + # Delete the route - response = self.client.delete('/api/routes/test.dev') + response = self.client.delete("/api/routes/test.dev") self.assertEqual(response.status_code, 204) - + # Also get the route and assert that it does not exist - response = self.client.get('/api/routes/test.dev') + response = self.client.get("/api/routes/test.dev") self.assertEqual(response.status_code, 404) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main()