├── 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 `&` 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 |
--------------------------------------------------------------------------------