├── README.md ├── lualib ├── monarch.lua ├── net │ └── url.lua └── resty │ └── hmac.lua └── nginx └── conf └── nginx.conf /README.md: -------------------------------------------------------------------------------- 1 | Monarch OpenResty-based API Gateway 2 | =================================== 3 | 4 | Works in tandem with the API Manager to authenticate and authorize requests before proxying them to the appropriate backend service. 5 | 6 | Simply overlay the following files into your [OpenResty](http://openresty.org "OpenResty home page") installation directory (e.g. /usr/local/openresty). 7 | 8 | - **lualib/monarch.lua** - The main gateway script that handles calling the Monarch API Manager to verify the incoming request. 9 | - **lualib/resty/hmac.lua** - A contributed script that adds HMAC support. 10 | - **lualib/net/url.lua** - A contributed script that parses URLs. 11 | - **nginx/conf/nginx.conf** - The API gateway Nginx configuration. 12 | 13 | Next, you will need to edit nginx.conf to configure the following values: 14 | 15 | - **Host** and **Environment ID** - Under `location = /monarch_auth` and `location = /monarch_traffic` you will need to update the `Host` and `X-Environment-Id` headers. The Monarch environment ID can be accessed in the admin console by clicking the gear icon next to your user name in the upper right-hand corner. Likewise, the `upstream monarch_backend` needs to have the correct host and port. 16 | - **Provider Key** and **Shared Secret** - You will need to create a new Provider in the admin console (e.g. name = gateway, permissions = Authenticate API requests) and change the `set $provider_key "XXXX";` in nginx.conf to have the correct provider API key and shared secret you created for this provider. 17 | 18 | Refer to the `location /` for how to enable the communication with Monarch. You can find documentation on configuring Nginx [on their wiki](http://wiki.nginx.org/Configuration "Nginx configuration"). -------------------------------------------------------------------------------- /lualib/monarch.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2015 CapTech Ventures, Inc. 2 | -- (http://www.captechconsulting.com) All Rights Reserved. 3 | -- 4 | -- Licensed under the Apache License, Version 2.0 (the "License"); 5 | -- you may not use this file except in compliance with the License. 6 | -- You may obtain a copy of the License at 7 | -- 8 | -- http://www.apache.org/licenses/LICENSE-2.0 9 | -- 10 | -- Unless required by applicable law or agreed to in writing, software 11 | -- distributed under the License is distributed on an "AS IS" BASIS, 12 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | -- See the License for the specific language governing permissions and 14 | -- limitations under the License. 15 | 16 | local M = { 17 | _VERSION = '0.1', 18 | _HEADER_VERSION = 1 19 | } 20 | 21 | local resty_sha256 = require "resty.sha256" 22 | local resty_hmac = require "resty.hmac" 23 | local url = require "net.url" 24 | local cjson = require "cjson" 25 | 26 | local function random_alphanumeric(length) 27 | local random = ""; 28 | 29 | for i = 1, length do 30 | local n = math.random(55, 116); 31 | 32 | if n < 65 then 33 | n = n - 7; 34 | elseif n > 90 then 35 | n = n + 6; 36 | end 37 | 38 | random = random .. string.char(n); 39 | end 40 | 41 | return random; 42 | end 43 | 44 | local function get_port(host_header, scheme) 45 | local port = host_header:match(":(%d+)$"); 46 | 47 | if port == nil then 48 | if scheme == "http" then 49 | port = 80; 50 | elseif scheme == "https" then 51 | port = 443; 52 | end 53 | else 54 | port = tonumber(port); 55 | end 56 | 57 | return port; 58 | end 59 | 60 | local function get_mime_type(content_type) 61 | local mimeType = "" 62 | 63 | if (content_type ~= nil) then 64 | mimeType = content_type; 65 | 66 | local semi = string.find(mimeType, ";") 67 | 68 | if semi ~= nil then 69 | mimeType = string.gsub(mimeType.sub(1, semi - 1), "%s$", ""); 70 | end 71 | end 72 | 73 | return mimeType; 74 | end 75 | 76 | local function empty_string_if_null(value) 77 | if (value ~= nil) then 78 | return value; 79 | else 80 | return ""; 81 | end 82 | end 83 | 84 | local function empty_string_to_null(value) 85 | if (value ~= nil and value == "") then 86 | return cjson.null; 87 | else 88 | if (value == nil) then 89 | return cjson.null; 90 | else 91 | return value; 92 | end 93 | end 94 | end 95 | 96 | 97 | local function is_defined(value) 98 | return (value ~= null or value == cjson.null); 99 | end 100 | 101 | local function new_line_delimited(table_value) 102 | return table.concat(table_value, "\n") .. "\n"; 103 | end 104 | 105 | local function calc_sha256_base64_encoded(data) 106 | local sha256 = resty_sha256:new(); 107 | sha256:update(data); 108 | local digest = sha256:final(); 109 | 110 | return ngx.encode_base64(digest); 111 | end 112 | 113 | local function calc_hmac_base64_encoded(data, secret) 114 | local hmac = resty_hmac:new() 115 | local digest = hmac:digest("sha256", secret, data, true) 116 | 117 | return ngx.encode_base64(digest); 118 | end 119 | 120 | local function add_cors() 121 | if ngx.var.http_origin ~= nil then 122 | ngx.header["Access-Control-Allow-Origin"] = ngx.var.http_origin; 123 | ngx.header["Access-Control-Allow-Methods"] = "GET, POST, HEAD, OPTIONS, DELETE, PUT, PATCH"; 124 | ngx.header["Access-Control-Allow-Headers"] = "Content-Type, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Api-Key, X-Api-Key, Authorization"; 125 | ngx.header["Access-Control-Allow-Credentials"] = "true"; 126 | ngx.header["Access-Control-Max-Age"] = "1728000"; 127 | end 128 | end 129 | 130 | local function calc_payload_hash(mime_type, content) 131 | local payload = new_line_delimited({ 132 | "hawk.1.payload", 133 | mime_type, 134 | content 135 | }); 136 | 137 | return calc_sha256_base64_encoded(payload); 138 | end 139 | 140 | ------------- 141 | 142 | M.authenticate = function() 143 | local method = ngx.req.get_method(); 144 | local scheme = ngx.var.scheme; 145 | local host = ngx.var.host; 146 | local port = get_port(ngx.var.http_host, scheme); 147 | local uri = ngx.var.request_uri; 148 | local path = ngx.var.uri; 149 | local querystring = ngx.var.query_string; 150 | local headers = ngx.req.get_headers(); 151 | local headersArys = {}; 152 | local mimeType = get_mime_type(ngx.var.content_type); 153 | local content = empty_string_if_null(ngx.var.request_body); 154 | 155 | local bytes_received = string.len(method) + string.len(uri) + string.len(content) + 11; 156 | 157 | for k, v in pairs(headers) do 158 | headersArys[k] = {v}; 159 | bytes_received = bytes_received + string.len(k) + string.len(v) + 2; 160 | end 161 | 162 | local payload_hashes = {}; 163 | 164 | payload_hashes["Hawk V1"] = { 165 | sha256 = calc_payload_hash(mimeType, content) 166 | }; 167 | 168 | local authenticate_request = cjson.encode({ 169 | method = method, 170 | protocol = scheme, 171 | host = host, 172 | port = port, 173 | path = path, 174 | querystring = querystring, 175 | headers = headersArys, 176 | ipAddress = ngx.var.remote_addr, 177 | payloadHashes = payload_hashes, 178 | requestWeight = 1.0, 179 | performAuthorization = true, 180 | useLoadBalancer = true 181 | }); 182 | 183 | local ts = os.time(); 184 | local nonce = random_alphanumeric(6); 185 | 186 | local payload_hash = calc_payload_hash("application/json", authenticate_request); 187 | 188 | local header = new_line_delimited({ 189 | 'hawk.1.header', 190 | tostring(ts), 191 | nonce, 192 | "POST", 193 | "/service/v1/requests/authenticate", 194 | "monarch_backend", 195 | tostring(8000), 196 | payload_hash, 197 | "" 198 | }); 199 | 200 | local header_hash = calc_hmac_base64_encoded(header, ngx.var.shared_secret); 201 | local authorization = 'Hawk id="' .. ngx.var.provider_key .. '", ts="' .. ts .. '", nonce="' .. nonce .. '", hash="' .. payload_hash .. '"' .. ', mac="' .. header_hash .. '"'; 202 | 203 | local res = ngx.location.capture("/monarch_auth", { 204 | method = ngx.HTTP_POST, 205 | body = authenticate_request, 206 | vars = { 207 | authorization = authorization 208 | } 209 | }); 210 | 211 | ngx.var.bytes_received = bytes_received; 212 | ngx.var.real_path = path; 213 | ngx.var.real_args = ngx.var.args; 214 | 215 | if res.status ~= 200 then 216 | ngx.status = 503; 217 | 218 | add_cors(); 219 | 220 | ngx.header.content_type = "application/json"; 221 | ngx.print("{\"code\":503,\"reason\":\"systemUnavailable\",\"message\":\"The system is currently unavailable\",\"developerMessage\":\"Please try again later.\",\"errorCode\":\"SYS-0001\" }"); 222 | return; 223 | else 224 | local authResp = cjson.decode(res.body); 225 | ngx.log(ngx.INFO, res.body); 226 | local vars = authResp.vars; 227 | 228 | ngx.var.provider_id = vars.providerId; 229 | 230 | if type(vars.serviceId) == "string" then 231 | ngx.var.service_id = vars.serviceId; 232 | end 233 | 234 | if type(vars.serviceVersion) == "string" then 235 | ngx.var.service_version = vars.serviceVersion; 236 | end 237 | 238 | if type(vars.operation) == "string" then 239 | ngx.var.operation_name = vars.operation; 240 | end 241 | 242 | if type(authResp.target) == "string" then 243 | ngx.var.target = authResp.target 244 | end 245 | 246 | ngx.var.token_id = nil; 247 | ngx.var.user_id = nil; 248 | 249 | if authResp.code ~= 200 then 250 | ngx.var.reason = authResp.reason; 251 | ngx.status = authResp.code; 252 | 253 | add_cors(); 254 | 255 | ngx.log(ngx.ERR, "Authentication failed"); 256 | ngx.header.content_type = res.header["Content-Type"]; 257 | 258 | if is_defined(authResp.responseHeaders) then 259 | local headers = authResp.responseHeaders; 260 | 261 | for i, hdr in ipairs(headers) do 262 | ngx.header[hdr.name] = hdr.value; 263 | end 264 | end 265 | 266 | if is_defined(authResp.vars) then 267 | local error_response = cjson.encode({ 268 | code = authResp.code, 269 | reason = authResp.reason, 270 | message = authResp.message, 271 | developerMessage = authResp.developerMessage, 272 | errorCode = authResp.errorCode 273 | }); 274 | 275 | ngx.print(error_response); 276 | else 277 | ngx.print(res.body); 278 | end 279 | 280 | return; 281 | else 282 | local claims = authResp.claims; 283 | 284 | ngx.var.reason = "ok"; 285 | 286 | if is_defined(claims) then 287 | local application = claims["http://monarchapis.com/claims/application"]; 288 | local client = claims["http://monarchapis.com/claims/client"]; 289 | local token = claims["http://monarchapis.com/claims/token"]; 290 | local principal = claims["http://monarchapis.com/claims/principal"]; 291 | 292 | ngx.var.application_id = (is_defined(application) and application.id or nil) 293 | ngx.var.client_id = (is_defined(client) and client.id or nil) 294 | 295 | if is_defined(token) and type(token.id) == "string" then 296 | ngx.var.token_id = token.id; 297 | end 298 | 299 | if is_defined(principal) and type(principal.id) == "string" then 300 | ngx.var.user_id = principal.id; 301 | end 302 | end 303 | 304 | if is_defined(authResp.tokens) then 305 | local tokens = authResp.tokens; 306 | 307 | if is_defined(tokens.jwt) then 308 | ngx.req.set_header("Authorization", "Bearer " .. tokens.jwt); 309 | end 310 | end 311 | end 312 | end 313 | 314 | ngx.log(ngx.INFO, "Authentication succeeded"); 315 | end 316 | 317 | M.send_traffic = function() 318 | local method = ngx.req.get_method(); 319 | local scheme = ngx.var.scheme; 320 | local host = ngx.var.host; 321 | local port = get_port(ngx.var.http_host, scheme); 322 | local uri = ngx.var.request_uri; 323 | local path = ngx.var.real_path; 324 | local querystring = ngx.var.query_string; 325 | local headers = ngx.req.get_headers(); 326 | 327 | local headersArys = {}; 328 | for k, v in pairs(headers) do 329 | headersArys[k] = {v}; 330 | end 331 | 332 | local pars = nil; 333 | 334 | if ngx.var.real_args ~= nil then 335 | pars = url.parseQuery(ngx.var.real_args); 336 | end 337 | 338 | local response_time = math.ceil((ngx.now() - ngx.req.start_time()) * 1000); 339 | 340 | local event_request = cjson.encode({ 341 | request_id = nil, 342 | application_id = empty_string_to_null(ngx.var.application_id), 343 | client_id = empty_string_to_null(ngx.var.client_id), 344 | service_id = empty_string_to_null(ngx.var.service_id), 345 | service_version = empty_string_to_null(ngx.var.service_version), 346 | operation_name = empty_string_to_null(ngx.var.operation_name), 347 | provider_id = empty_string_to_null(ngx.var.provider_id), 348 | request_size = tonumber(ngx.var.bytes_received), 349 | response_size = tonumber(ngx.var.bytes_sent), 350 | response_time = response_time, 351 | status_code = tonumber(ngx.var.status), 352 | error_reason = empty_string_to_null(ngx.var.reason), 353 | cache_hit = false, 354 | token_id = empty_string_to_null(ngx.var.token_id), 355 | user_id = empty_string_to_null(ngx.var.user_id), 356 | host = host, 357 | path = path, 358 | port = port, 359 | verb = method, 360 | parameters = pars, 361 | headers = headers, 362 | client_ip = ngx.var.remote_addr, 363 | user_agent = ngx.var.http_user_agent 364 | }); 365 | 366 | ngx.log(ngx.INFO, event_request); 367 | 368 | local ts = os.time(); 369 | local nonce = random_alphanumeric(6); 370 | 371 | local payload_hash = calc_payload_hash("application/json", event_request); 372 | 373 | local header = new_line_delimited({ 374 | 'hawk.1.header', 375 | tostring(ts), 376 | nonce, 377 | "POST", 378 | "/analytics/v1/traffic/events", 379 | "monarch_backend", 380 | tostring(8000), 381 | payload_hash, 382 | "" 383 | }); 384 | 385 | local header_hash = calc_hmac_base64_encoded(header, ngx.var.shared_secret); 386 | local authorization = 'Hawk id="' .. ngx.var.provider_key .. '", ts="' .. ts .. '", nonce="' .. nonce .. '", hash="' .. payload_hash .. '"' .. ', mac="' .. header_hash .. '"'; 387 | 388 | local res = ngx.location.capture("/monarch_traffic", { 389 | method = ngx.HTTP_POST, 390 | body = event_request, 391 | vars = { 392 | authorization = authorization 393 | } 394 | }); 395 | 396 | if res.status ~= 204 then 397 | ngx.log(ngx.ERR, "Failed to log traffic"); 398 | else 399 | ngx.log(ngx.INFO, "Logged traffic successfully"); 400 | end 401 | end 402 | 403 | return M; 404 | 405 | -------------------------------------------------------------------------------- /lualib/net/url.lua: -------------------------------------------------------------------------------- 1 | -- neturl.lua - a robust url parser and builder 2 | -- 3 | -- Bertrand Mansion, 2011-2013; License MIT 4 | -- @module neturl 5 | -- @alias M 6 | 7 | local M = {} 8 | M.version = "0.9.0" 9 | 10 | --- url options 11 | -- separator is set to `&` by default but could be anything like `&amp;` or `;` 12 | -- @todo Add an option to limit the size of the argument table 13 | M.options = { 14 | separator = '&' 15 | } 16 | 17 | --- list of known and common scheme ports 18 | -- as documented in IANA URI scheme list 19 | M.services = { 20 | acap = 674, 21 | cap = 1026, 22 | dict = 2628, 23 | ftp = 21, 24 | gopher = 70, 25 | http = 80, 26 | https = 443, 27 | iax = 4569, 28 | icap = 1344, 29 | imap = 143, 30 | ipp = 631, 31 | ldap = 389, 32 | mtqp = 1038, 33 | mupdate = 3905, 34 | news = 2009, 35 | nfs = 2049, 36 | nntp = 119, 37 | rtsp = 554, 38 | sip = 5060, 39 | snmp = 161, 40 | telnet = 23, 41 | tftp = 69, 42 | vemmi = 575, 43 | afs = 1483, 44 | jms = 5673, 45 | rsync = 873, 46 | prospero = 191, 47 | videotex = 516 48 | } 49 | 50 | local legal = { 51 | ["-"] = true, ["_"] = true, ["."] = true, ["!"] = true, 52 | ["~"] = true, ["*"] = true, ["'"] = true, ["("] = true, 53 | [")"] = true, [":"] = true, ["@"] = true, ["&"] = true, 54 | ["="] = true, ["+"] = true, ["$"] = true, [","] = true, 55 | [";"] = true -- can be used for parameters in path 56 | } 57 | 58 | local function decode(str) 59 | local str = str:gsub('+', ' ') 60 | return (str:gsub("%%(%x%x)", function(c) 61 | return string.char(tonumber(c, 16)) 62 | end)) 63 | end 64 | 65 | local function encode(str) 66 | return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v) 67 | return string.upper(string.format("%%%02x", string.byte(v))) 68 | end)) 69 | end 70 | 71 | -- for query values, prefer + instead of %20 for spaces 72 | local function encodeValue(str) 73 | local str = encode(str) 74 | return str:gsub('%%20', '+') 75 | end 76 | 77 | local function encodeSegment(s) 78 | local legalEncode = function(c) 79 | if legal[c] then 80 | return c 81 | end 82 | return encode(c) 83 | end 84 | return s:gsub('([^a-zA-Z0-9])', legalEncode) 85 | end 86 | 87 | --- builds the url 88 | -- @return a string representing the built url 89 | function M:build() 90 | local url = '' 91 | if self.path then 92 | local path = self.path 93 | path:gsub("([^/]+)", function (s) return encodeSegment(s) end) 94 | url = url .. tostring(path) 95 | end 96 | if self.query then 97 | local qstring = tostring(self.query) 98 | if qstring ~= "" then 99 | url = url .. '?' .. qstring 100 | end 101 | end 102 | if self.host then 103 | local authority = self.host 104 | if self.port and self.scheme and M.services[self.scheme] ~= self.port then 105 | authority = authority .. ':' .. self.port 106 | end 107 | local userinfo 108 | if self.user and self.user ~= "" then 109 | userinfo = self.user 110 | if self.password then 111 | userinfo = userinfo .. ':' .. self.password 112 | end 113 | end 114 | if userinfo and userinfo ~= "" then 115 | authority = userinfo .. '@' .. authority 116 | end 117 | if authority then 118 | if url ~= "" then 119 | url = '//' .. authority .. '/' .. url:gsub('^/+', '') 120 | else 121 | url = '//' .. authority 122 | end 123 | end 124 | end 125 | if self.scheme then 126 | url = self.scheme .. ':' .. url 127 | end 128 | if self.fragment then 129 | url = url .. '#' .. self.fragment 130 | end 131 | return url 132 | end 133 | 134 | --- builds the querystring 135 | -- @param tab The key/value parameters 136 | -- @param sep The separator to use (optional) 137 | -- @param key The parent key if the value is multi-dimensional (optional) 138 | -- @return a string representing the built querystring 139 | function M.buildQuery(tab, sep, key) 140 | local query = {} 141 | if not sep then 142 | sep = M.options.separator or '&' 143 | end 144 | local keys = {} 145 | for k in pairs(tab) do 146 | keys[#keys+1] = k 147 | end 148 | table.sort(keys) 149 | for _,name in ipairs(keys) do 150 | local value = tab[name] 151 | name = encode(tostring(name)) 152 | if key then 153 | name = string.format('%s[%s]', tostring(key), tostring(name)) 154 | end 155 | if type(value) == 'table' then 156 | query[#query+1] = M.buildQuery(value, sep, name) 157 | else 158 | local value = encodeValue(tostring(value)) 159 | if value ~= "" then 160 | query[#query+1] = string.format('%s=%s', name, value) 161 | else 162 | query[#query+1] = name 163 | end 164 | end 165 | end 166 | return table.concat(query, sep) 167 | end 168 | 169 | --- Parses the querystring to a table 170 | -- This function can parse multidimensional pairs and is mostly compatible 171 | -- with PHP usage of brackets in key names like ?param[key]=value 172 | -- @param str The querystring to parse 173 | -- @param sep The separator between key/value pairs, defaults to `&` 174 | -- @todo limit the max number of parameters with M.options.max_parameters 175 | -- @return a table representing the query key/value pairs 176 | function M.parseQuery(str, sep) 177 | if not sep then 178 | sep = M.options.separator or '&' 179 | end 180 | 181 | local values = {} 182 | for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do 183 | local key = decode(key) 184 | local keys = {} 185 | key = key:gsub('%[([^%]]*)%]', function(v) 186 | -- extract keys between balanced brackets 187 | if string.find(v, "^-?%d+$") then 188 | v = tonumber(v) 189 | else 190 | v = decode(v) 191 | end 192 | table.insert(keys, v) 193 | return "=" 194 | end) 195 | key = key:gsub('=+.*$', "") 196 | key = key:gsub('%s', "_") -- remove spaces in parameter name 197 | val = val:gsub('^=+', "") 198 | 199 | if not values[key] then 200 | values[key] = {} 201 | end 202 | if #keys > 0 and type(values[key]) ~= 'table' then 203 | values[key] = {} 204 | elseif #keys == 0 and type(values[key]) == 'table' then 205 | values[key] = decode(val) 206 | end 207 | 208 | local t = values[key] 209 | for i,k in ipairs(keys) do 210 | if type(t) ~= 'table' then 211 | t = {} 212 | end 213 | if k == "" then 214 | k = #t+1 215 | end 216 | if not t[k] then 217 | t[k] = {} 218 | end 219 | if i == #keys then 220 | t[k] = decode(val) 221 | end 222 | t = t[k] 223 | end 224 | end 225 | setmetatable(values, { __tostring = M.buildQuery }) 226 | return values 227 | end 228 | 229 | --- set the url query 230 | -- @param query Can be a string to parse or a table of key/value pairs 231 | -- @return a table representing the query key/value pairs 232 | function M:setQuery(query) 233 | local query = query 234 | if type(query) == 'table' then 235 | query = M.buildQuery(query) 236 | end 237 | self.query = M.parseQuery(query) 238 | return query 239 | end 240 | 241 | --- set the authority part of the url 242 | -- The authority is parsed to find the user, password, port and host if available. 243 | -- @param authority The string representing the authority 244 | -- @return a string with what remains after the authority was parsed 245 | function M:setAuthority(authority) 246 | self.authority = authority 247 | self.port = nil 248 | self.host = nil 249 | self.userinfo = nil 250 | self.user = nil 251 | self.password = nil 252 | 253 | authority = authority:gsub('^([^@]*)@', function(v) 254 | self.userinfo = v 255 | return '' 256 | end) 257 | authority = authority:gsub("^%[[^%]]+%]", function(v) 258 | -- ipv6 259 | self.host = v 260 | return '' 261 | end) 262 | authority = authority:gsub(':([^:]*)$', function(v) 263 | self.port = tonumber(v) 264 | return '' 265 | end) 266 | if authority ~= '' and not self.host then 267 | self.host = authority:lower() 268 | end 269 | if self.userinfo then 270 | local userinfo = self.userinfo 271 | userinfo = userinfo:gsub(':([^:]*)$', function(v) 272 | self.password = v 273 | return '' 274 | end) 275 | self.user = userinfo 276 | end 277 | return authority 278 | end 279 | 280 | --- Parse the url into the designated parts. 281 | -- Depending on the url, the following parts can be available: 282 | -- scheme, userinfo, user, password, authority, host, port, path, 283 | -- query, fragment 284 | -- @param url Url string 285 | -- @return a table with the different parts and a few other functions 286 | function M.parse(url) 287 | local comp = {} 288 | M.setAuthority(comp, "") 289 | M.setQuery(comp, "") 290 | 291 | local url = tostring(url or '') 292 | url = url:gsub('#(.*)$', function(v) 293 | comp.fragment = v 294 | return '' 295 | end) 296 | url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v) 297 | comp.scheme = v:lower() 298 | return '' 299 | end) 300 | url = url:gsub('%?(.*)', function(v) 301 | M.setQuery(comp, v) 302 | return '' 303 | end) 304 | url = url:gsub('^//([^/]*)', function(v) 305 | M.setAuthority(comp, v) 306 | return '' 307 | end) 308 | comp.path = decode(url) 309 | 310 | setmetatable(comp, { 311 | __index = M, 312 | __tostring = M.build} 313 | ) 314 | return comp 315 | end 316 | 317 | --- removes dots and slashes in urls when possible 318 | -- This function will also remove multiple slashes 319 | -- @param path The string representing the path to clean 320 | -- @return a string of the path without unnecessary dots and segments 321 | function M.removeDotSegments(path) 322 | local fields = {} 323 | if string.len(path) == 0 then 324 | return "" 325 | end 326 | local startslash = false 327 | local endslash = false 328 | if string.sub(path, 1, 1) == "/" then 329 | startslash = true 330 | end 331 | if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then 332 | endslash = true 333 | end 334 | 335 | path:gsub('[^/]+', function(c) table.insert(fields, c) end) 336 | 337 | local new = {} 338 | local j = 0 339 | 340 | for i,c in ipairs(fields) do 341 | if c == '..' then 342 | if j > 0 then 343 | j = j - 1 344 | end 345 | elseif c ~= "." then 346 | j = j + 1 347 | new[j] = c 348 | end 349 | end 350 | local ret = "" 351 | if #new > 0 and j > 0 then 352 | ret = table.concat(new, '/', 1, j) 353 | else 354 | ret = "" 355 | end 356 | if startslash then 357 | ret = '/'..ret 358 | end 359 | if endslash then 360 | ret = ret..'/' 361 | end 362 | return ret 363 | end 364 | 365 | local function absolutePath(base_path, relative_path) 366 | if string.sub(relative_path, 1, 1) == "/" then 367 | return '/' .. string.gsub(relative_path, '^[%./]+', '') 368 | end 369 | local path = base_path 370 | if relative_path ~= "" then 371 | path = '/'..path:gsub("[^/]*$", "") 372 | end 373 | path = path .. relative_path 374 | path = path:gsub("([^/]*%./)", function (s) 375 | if s ~= "./" then return s else return "" end 376 | end) 377 | path = string.gsub(path, "/%.$", "/") 378 | local reduced 379 | while reduced ~= path do 380 | reduced = path 381 | path = string.gsub(reduced, "([^/]*/%.%./)", function (s) 382 | if s ~= "../../" then return "" else return s end 383 | end) 384 | end 385 | path = string.gsub(path, "([^/]*/%.%.?)$", function (s) 386 | if s ~= "../.." then return "" else return s end 387 | end) 388 | local reduced 389 | while reduced ~= path do 390 | reduced = path 391 | path = string.gsub(reduced, '^/?%.%./', '') 392 | end 393 | return '/' .. path 394 | end 395 | 396 | --- builds a new url by using the one given as parameter and resolving paths 397 | -- @param other A string or a table representing a url 398 | -- @return a new url table 399 | function M:resolve(other) 400 | if type(self) == "string" then 401 | self = M.parse(self) 402 | end 403 | if type(other) == "string" then 404 | other = M.parse(other) 405 | end 406 | if other.scheme then 407 | return other 408 | else 409 | other.scheme = self.scheme 410 | if not other.authority or other.authority == "" then 411 | other:setAuthority(self.authority) 412 | if not other.path or other.path == "" then 413 | other.path = self.path 414 | local query = other.query 415 | if not query or not next(query) then 416 | other.query = self.query 417 | end 418 | else 419 | other.path = absolutePath(self.path, other.path) 420 | end 421 | end 422 | return other 423 | end 424 | end 425 | 426 | --- normalize a url path following some common normalization rules 427 | -- described on The URL normalization page of Wikipedia 428 | -- @return the normalized path 429 | function M:normalize() 430 | if type(self) == 'string' then 431 | self = M.parse(self) 432 | end 433 | if self.path then 434 | local path = self.path 435 | path = absolutePath(path, "") 436 | -- normalize multiple slashes 437 | path = string.gsub(path, "//+", "/") 438 | self.path = path 439 | end 440 | return self 441 | end 442 | 443 | return M -------------------------------------------------------------------------------- /lualib/resty/hmac.lua: -------------------------------------------------------------------------------- 1 | -- Adds HMAC support to Lua with multiple algorithms, via OpenSSL and FFI 2 | -- 3 | -- Author: ddragosd@gmail.com 4 | -- Date: 16/05/14 5 | -- 6 | 7 | 8 | local ffi = require "ffi" 9 | local ffi_new = ffi.new 10 | local ffi_str = ffi.string 11 | local C = ffi.C 12 | local resty_string = require "resty.string" 13 | local setmetatable = setmetatable 14 | local error = error 15 | 16 | 17 | local _M = { _VERSION = '0.09' } 18 | 19 | 20 | local mt = { __index = _M } 21 | 22 | -- 23 | -- EVP_MD is defined in openssl/evp.h 24 | -- HMAC is defined in openssl/hmac.h 25 | -- 26 | ffi.cdef[[ 27 | typedef struct env_md_st EVP_MD; 28 | typedef struct env_md_ctx_st EVP_MD_CTX; 29 | unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len, 30 | const unsigned char *d, size_t n, unsigned char *md, 31 | unsigned int *md_len); 32 | const EVP_MD *EVP_sha1(void); 33 | const EVP_MD *EVP_sha224(void); 34 | const EVP_MD *EVP_sha256(void); 35 | const EVP_MD *EVP_sha384(void); 36 | const EVP_MD *EVP_sha512(void); 37 | ]] 38 | 39 | -- table definind the available algorithms and the length of each digest 40 | -- for more information @see: http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf 41 | local available_algorithms = { 42 | sha1 = { alg = C.EVP_sha1(), length = 160/8 }, 43 | sha224 = { alg = C.EVP_sha224(), length = 224/8 }, 44 | sha256 = { alg = C.EVP_sha256(), length = 256/8 }, 45 | sha384 = { alg = C.EVP_sha384(), length = 384/8 }, 46 | sha512 = { alg = C.EVP_sha512(), length = 512/8 } 47 | } 48 | 49 | -- 64 is the max lenght and it covers up to sha512 algorithm 50 | local digest_len = ffi_new("int[?]", 64) 51 | local buf = ffi_new("char[?]", 64) 52 | 53 | 54 | function _M.new(self) 55 | return setmetatable({}, mt) 56 | end 57 | 58 | local function getDigestAlgorithm(dtype) 59 | local md_name = available_algorithms[dtype] 60 | if ( md_name == nil ) then 61 | error("attempt to use unknown algorithm: '" .. dtype .. 62 | "'.\n Available algorithms are: sha1,sha224,sha256,sha384,sha512") 63 | end 64 | return md_name.alg, md_name.length 65 | end 66 | 67 | --- 68 | -- Returns the HMAC digest. The hashing algorithm is defined by the dtype parameter. 69 | -- The optional raw flag, defaulted to false, is a boolean indicating whether the output should be a direct binary 70 | -- equivalent of the HMAC or formatted as a hexadecimal string (the default) 71 | -- 72 | -- @param self 73 | -- @param dtype The hashing algorithm to use is specified by dtype 74 | -- @param key The secret 75 | -- @param msg The message to be signed 76 | -- @param raw When true, it returns the binary format, else, the hex format is returned 77 | -- 78 | function _M.digest(self, dtype, key, msg, raw) 79 | local evp_md, digest_length_int = getDigestAlgorithm(dtype) 80 | if key == nil or msg == nil then 81 | error("attempt to digest with a null key or message") 82 | end 83 | 84 | C.HMAC(evp_md, key, #key, msg, #msg, buf, digest_len) 85 | 86 | if raw == true then 87 | return ffi_str(buf,digest_length_int) 88 | end 89 | 90 | return resty_string.to_hex(ffi_str(buf,digest_length_int)) 91 | end 92 | 93 | 94 | return _M 95 | -------------------------------------------------------------------------------- /nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | #user nobody; 3 | worker_processes 1; 4 | 5 | #error_log logs/error.log error; 6 | #error_log logs/error.log notice; 7 | #error_log logs/error.log info; 8 | 9 | #pid logs/nginx.pid; 10 | 11 | 12 | events { 13 | worker_connections 1024; 14 | } 15 | 16 | 17 | http { 18 | include mime.types; 19 | default_type application/octet-stream; 20 | 21 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | # '$status $body_bytes_sent "$http_referer" ' 23 | # '"$http_user_agent" "$http_x_forwarded_for"'; 24 | 25 | #access_log logs/access.log main; 26 | 27 | sendfile on; 28 | #tcp_nopush on; 29 | 30 | #keepalive_timeout 0; 31 | keepalive_timeout 65; 32 | 33 | #gzip on; 34 | 35 | init_by_lua 'monarch = require "monarch"'; 36 | 37 | # Change this to the list of monarch servers 38 | upstream monarch_backend { 39 | server localhost:8000; 40 | } 41 | 42 | # Change this to the list of API servers 43 | upstream api_backend { 44 | server localhost:8080; 45 | } 46 | 47 | server { 48 | listen 80; 49 | server_name localhost; 50 | 51 | #access_log logs/host.access.log main; 52 | 53 | location / { 54 | set $provider_key "XXXX"; 55 | set $shared_secret "XXXX"; 56 | 57 | set $test ""; 58 | 59 | # Analytics variables that carry over to post action 60 | set $provider_id ""; 61 | set $application_id ""; 62 | set $client_id ""; 63 | set $service_id ""; 64 | set $service_version ""; 65 | set $operation_name ""; 66 | set $reason ""; 67 | set $token_id ""; 68 | set $user_id ""; 69 | set $bytes_received 0; 70 | set $real_path ""; 71 | set $real_args ""; 72 | 73 | # CORS 74 | if ($request_method = OPTIONS) { 75 | set $test "1"; 76 | } 77 | 78 | # Change this to the expected API hostname:port 79 | if ($http_origin ~* (localhost\:80)) { 80 | set $test "${test}1"; 81 | } 82 | 83 | if ($test = 11) { 84 | add_header "Access-Control-Allow-Origin" $http_origin; 85 | add_header 'Access-Control-Allow-Methods' 'GET, POST, HEAD, OPTIONS, DELETE, PUT, PATCH'; 86 | add_header 'Access-Control-Allow-Headers' 'Content-Type, Accept, Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Api-Key, X-Api-Key, Authorization'; 87 | add_header 'Access-Control-Allow-Credentials' 'true'; 88 | add_header 'Access-Control-Max-Age' '1728000'; 89 | return 200; 90 | } 91 | 92 | # Proxy config 93 | # If a load balancer is enabled in Monarch, this value will be overridden. 94 | set $target "api_backend"; 95 | access_by_lua "monarch.authenticate()"; 96 | 97 | proxy_set_header Host $host; 98 | proxy_set_header X-Forwarded-For $remote_addr; 99 | proxy_set_header X-Forwarded-Proto $scheme; 100 | proxy_set_header X-Forwarded-Path $uri; 101 | proxy_pass http://$target; 102 | 103 | post_action /monarch_post_action; 104 | } 105 | 106 | # 107 | # INTERNAL MONARCH LOCATIONS 108 | # 109 | 110 | location = /monarch_auth { 111 | internal; 112 | 113 | proxy_pass http://monarch_backend/service/v1/requests/authenticate; 114 | proxy_pass_request_headers off; 115 | proxy_set_header Host "monarch_backend:8000"; 116 | proxy_set_header X-Environment-Id "XXXX"; 117 | proxy_set_header Accept "application/json"; 118 | proxy_set_header Content-Type "application/json"; 119 | proxy_set_header Authorization $authorization; 120 | } 121 | 122 | location = /monarch_traffic { 123 | internal; 124 | 125 | proxy_pass http://monarch_backend/analytics/v1/traffic/events; 126 | proxy_pass_request_headers off; 127 | proxy_set_header Host "monarch_backend:8000"; 128 | proxy_set_header X-Environment-Id "XXXX"; 129 | proxy_set_header Accept "application/json"; 130 | proxy_set_header Content-Type "application/json"; 131 | proxy_set_header Authorization $authorization; 132 | } 133 | 134 | location = /monarch_post_action { 135 | internal; 136 | set $authorization ""; 137 | 138 | content_by_lua "monarch.send_traffic()"; 139 | } 140 | 141 | #error_page 404 /404.html; 142 | 143 | # redirect server error pages to the static page /50x.html 144 | # 145 | error_page 500 502 503 504 /50x.html; 146 | location = /50x.html { 147 | root html; 148 | } 149 | 150 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 151 | # 152 | #location ~ \.php$ { 153 | # proxy_pass http://127.0.0.1; 154 | #} 155 | 156 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 157 | # 158 | #location ~ \.php$ { 159 | # root html; 160 | # fastcgi_pass 127.0.0.1:9000; 161 | # fastcgi_index index.php; 162 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 163 | # include fastcgi_params; 164 | #} 165 | 166 | # deny access to .htaccess files, if Apache's document root 167 | # concurs with nginx's one 168 | # 169 | #location ~ /\.ht { 170 | # deny all; 171 | #} 172 | } 173 | 174 | 175 | # another virtual host using mix of IP-, name-, and port-based configuration 176 | # 177 | #server { 178 | # listen 8000; 179 | # listen somename:8080; 180 | # server_name somename alias another.alias; 181 | 182 | # location / { 183 | # root html; 184 | # index index.html index.htm; 185 | # } 186 | #} 187 | 188 | 189 | # HTTPS server 190 | # 191 | #server { 192 | # listen 443 ssl; 193 | # server_name localhost; 194 | 195 | # ssl_certificate cert.pem; 196 | # ssl_certificate_key cert.key; 197 | 198 | # ssl_session_cache shared:SSL:1m; 199 | # ssl_session_timeout 5m; 200 | 201 | # ssl_ciphers HIGH:!aNULL:!MD5; 202 | # ssl_prefer_server_ciphers on; 203 | 204 | # location / { 205 | # root html; 206 | # index index.html index.htm; 207 | # } 208 | #} 209 | 210 | } 211 | --------------------------------------------------------------------------------