From 9870c16f370a7ba6632227a7800d4fecaf87ea7e Mon Sep 17 00:00:00 2001 From: FORESTIER Fabien <fabien.forestier@soprasteria.com> Date: Mon, 27 Apr 2020 14:15:22 +0200 Subject: [PATCH] This proxy now has twho root routes /map and /download --- .gitlab-ci.yml | 4 +- Dockerfile | 4 +- README.md | 2 +- docker-compose.yml | 8 +- helpers/elasticsearch.helpers.js | 76 +++++ helpers/logs.helpers.js | 7 + helpers/redis.helpers.js | 61 ++++ helpers/request-processor.helper.js | 62 ++++ helpers/userAccesses.helpers.js | 31 ++ index.js | 299 ++++--------------- package-lock.json | 433 ++++++++++++++++++++++++++-- package.json | 11 +- routes/download.routes.js | 108 +++++++ routes/index.js | 6 + routes/map.routes.js | 76 +++++ template.env | 3 +- 16 files changed, 915 insertions(+), 276 deletions(-) create mode 100644 helpers/elasticsearch.helpers.js create mode 100644 helpers/logs.helpers.js create mode 100644 helpers/redis.helpers.js create mode 100644 helpers/request-processor.helper.js create mode 100644 helpers/userAccesses.helpers.js create mode 100644 routes/download.routes.js create mode 100644 routes/index.js create mode 100644 routes/map.routes.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63bbe7b..aafc60f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,7 +29,7 @@ build_development: - development script: - export TAG=$CI_COMMIT_SHORT_SHA - - export PROXY_MVT_BIND_PORT=9002 + - export PROXY_QUERY_BIND_PORT=9002 - docker-compose build - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker-compose push @@ -41,7 +41,7 @@ build_release: - tags script: - export TAG=$(echo $CI_COMMIT_TAG | sed 's/v//g') - - export PROXY_MVT_BIND_PORT=9002 + - export PROXY_QUERY_BIND_PORT=9002 - docker-compose build - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker-compose push \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8734e31..eabb7c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ WORKDIR /app # Install npm dependencies RUN npm install # Copy the project -COPY . /app +COPY ./helpers ./helpers +COPY ./routes ./routes +COPY ./index.js . CMD [ "npm", "start" ] \ No newline at end of file diff --git a/README.md b/README.md index 9503003..c97583f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Web mapping services proxy +# Query proxy ## Installation diff --git a/docker-compose.yml b/docker-compose.yml index d307d20..b3138d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,11 @@ version: "3" services: - proxy-mvt: - restart: unless-stopped + proxy-query: build: . image: registry.forge.grandlyon.com/web-et-numerique/web-et-numerique-internet/data.grandlyon.com/web-portal/components/proxies/web-mapping-services:${TAG} ports: - - ${PROXY_MVT_BIND_PORT}:9000 + - ${PROXY_QUERY_BIND_PORT}:9000 environment: - REDIS_SENTINEL_HOST=${REDIS_SENTINEL_HOST} - REDIS_SENTINEL_PORT=${REDIS_SENTINEL_PORT} @@ -14,12 +13,11 @@ services: - REDIS_AUTHORIZED_TTL=${REDIS_AUTHORIZED_TTL} - REDIS_UNAUTHORIZED_TTL=${REDIS_UNAUTHORIZED_TTL} - ELASTICSEARCH_URL=${ELASTICSEARCH_URL} + - LEGACY_AUTH_MIDDLEWARE_URL=${LEGACY_AUTH_MIDDLEWARE_URL} - TECHNICAL_ACCOUNT_USERNAME=${TECHNICAL_ACCOUNT_USERNAME} - TECHNICAL_ACCOUNT_PASSWORD=${TECHNICAL_ACCOUNT_PASSWORD} - PROXY_HOST_TARGET=${PROXY_HOST_TARGET} - IGN_KEY=${IGN_KEY} - depends_on: - - redis-sentinel-1 restart: unless-stopped redis-master: diff --git a/helpers/elasticsearch.helpers.js b/helpers/elasticsearch.helpers.js new file mode 100644 index 0000000..32bb3e6 --- /dev/null +++ b/helpers/elasticsearch.helpers.js @@ -0,0 +1,76 @@ +const axios = require('axios'); + +module.exports.getDatasetInfoFromES = async (elasticsearchUrl, layer, fields, cookies, pathToFile) => { + let options = { + method: 'POST', + url: `${elasticsearchUrl}/_search?request_cache=false`, + data: { + "from": 0, + "size": 1, + "_source": fields, + "query": { + "term": { + "metadata-fr.link.name.keyword": layer + } + } + }, + }; + + if (pathToFile) { + options = { + method: 'POST', + url: `${elasticsearchUrl}/_search?request_cache=false`, + data: { + "from": 0, + "size": 1, + "_source": fields, + "query": { + "bool": { + "should": [ + { + "term": { + "metadata-fr.link.name.keyword": layer + } + }, + { + "match_phrase": { + "metadata-fr.link.url": { + "query": pathToFile + } + } + } + ] + } + } + } + }; + } + + if (cookies) { + options.headers = { + cookie: cookies, + }; + } + + let response; + + try { + response = await axios(options) + } catch (err) { + throw { + err, + status: 500, + message: "Request to Elasticsearch failed", + }; + } + + // If no results are found it means the specified dataset mvt layer doesn't exists + if (response.data.hits.hits < 1) { + throw { + status: 404, + message: "Layer not found.", + }; + } + + return response.data.hits.hits[0]._source; +}; \ No newline at end of file diff --git a/helpers/logs.helpers.js b/helpers/logs.helpers.js new file mode 100644 index 0000000..17d5d98 --- /dev/null +++ b/helpers/logs.helpers.js @@ -0,0 +1,7 @@ +module.exports.printLog = (context, value) => { + console.log(`${new Date().toLocaleString("fr-FR")} [${context}] [log] `, value); +} + +module.exports.printError = (context, error) => { + console.error(`${new Date().toLocaleString("fr-FR")} [${context}] [error] `, error); +} \ No newline at end of file diff --git a/helpers/redis.helpers.js b/helpers/redis.helpers.js new file mode 100644 index 0000000..77986ee --- /dev/null +++ b/helpers/redis.helpers.js @@ -0,0 +1,61 @@ +const Redis = require("ioredis"); +const printError = require('./logs.helpers.js').printError; +const printLog = require('./logs.helpers.js').printLog; + +module.exports.setRedisValue = async (redisSentinelHost, redisSentinelPort, redisGroupName, key, value, ttl) => { + + const redisClient = new Redis({ + sentinels: [{ + host: redisSentinelHost, + port: redisSentinelPort + },], + name: redisGroupName, + }); + + redisClient.on('error', (error) => { + printError('Redis client', error); + redisClient.disconnect(); + }); + + + // Set key value with expiration time in seconds + const res = await redisClient.set(key, value, 'EX', ttl).catch((error) => { + redisClient.disconnect(); + printError('Redis client', 'Couldn\'t set redis key/value (with ttl).'); + printError('Redis client', error); + return false; + }); + + + printLog('Redis client', 'Done setting key/value pair'); + redisClient.disconnect(); + + return res ? true : false; +}; + +module.exports.getRedisValue = async (redisSentinelHost, redisSentinelPort, redisGroupName, key) => { + + const redisClient = new Redis({ + sentinels: [{ + host: redisSentinelHost, + port: redisSentinelPort + },], + name: redisGroupName, + }); + + redisClient.on('error', (error) => { + printError('Redis client', error); + redisClient.disconnect(); + }); + + const res = await redisClient.get(key).catch((error) => { + redisClient.disconnect(); + printError('Redis client', 'Couldn\'t get redis value.'); + printError('Redis client', error); + return false; + }); + + printLog(`Redis client`, `Value found ${res}`); + redisClient.disconnect(); + return res ? res : null; +}; \ No newline at end of file diff --git a/helpers/request-processor.helper.js b/helpers/request-processor.helper.js new file mode 100644 index 0000000..7da0bda --- /dev/null +++ b/helpers/request-processor.helper.js @@ -0,0 +1,62 @@ +const getDatasetInfoFromES = require('./elasticsearch.helpers.js').getDatasetInfoFromES; +const validateUserAccesses = require('./userAccesses.helpers.js').validateUserAccesses; +const getRedisValue = require('../helpers/redis.helpers.js').getRedisValue; +const setRedisValue = require('../helpers/redis.helpers.js').setRedisValue; + +module.exports.requestProcessor = async (req, res, layer, service, pathToFile) => { + const source = await getDatasetInfoFromES( + req.app.locals.config.elasticsearchUrl, + layer, + [ + "editorial-metadata.isOpenAccess" + ], + req.headers.cookie, + pathToFile + ); + + // If dataset is open access proxy the request without adding the technical account credentials + if (source['editorial-metadata'].isOpenAccess) { + req.app.locals.proxies.unauthenticated.web(req, res, {}); + return; + } + + // If it is a restricted access layer and the user isn't authenticated then directly send a 401 error + if (!source['editorial-metadata'].isOpenAccess && req.headers['x-anonymous-consumer']) { + throw { + status: 401, + message: "Unauthenticated, you need to be authenticated to access this resource." + } + } + + // Look for an existing value of the user rights for the layer in redis + let userAccesses = await getRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `download-${layer}-${service}-${req.headers['x-consumer-username']}`); + + // If value found and true, proxy the request adding the technical account credentials + if (userAccesses === 'true') { + req.app.locals.proxies.authenticated.web(req, res, {}); + return; + } + + // If value found and false, directly send a 403 forbidden error + if (userAccesses === 'false') { + throw { + status: 403, + message: "Forbidden access." + } + } + + // If no pre-existing value for that user, layer and service triple then check the user rights + userAccesses = await validateUserAccesses(req.app.locals.config.legacyAuthMiddlewareUrl, req.headers, `${req.params.repo}/${layer}`, service); + + if(!userAccesses) { + await setRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `download-${layer}-${service}-${req.headers['x-consumer-username']}`, false, req.app.locals.config.redisUnauthorizedTTL); + throw { + status: 403, + message: "Forbidden access." + } + } + + await setRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `download-${layer}-${service}-${req.headers['x-consumer-username']}`, true, req.app.locals.config.redisAuthorizedTTL); + req.app.locals.proxies.authenticated.web(req, res, {}); + return; +} \ No newline at end of file diff --git a/helpers/userAccesses.helpers.js b/helpers/userAccesses.helpers.js new file mode 100644 index 0000000..61ddfdf --- /dev/null +++ b/helpers/userAccesses.helpers.js @@ -0,0 +1,31 @@ +const axios = require('axios'); + +module.exports.validateUserAccesses = async (legacyAuthMiddlewareUrl, headers, pathToLayer, service) => { + const options = {}; + + if (headers.cookie) { + options.Cookie = headers.cookie; + } + + if(headers["x-xsrf-token"]) { + options["x-xsrf-token"] = headers['x-xsrf-token']; + } + + const response = await axios.get(`${legacyAuthMiddlewareUrl}/user/resources`, { + headers: options, + }).catch((err) => { + throw { + err, + status: err.response.status, + message: "Request to Legacy Auth Middleware failed", + }; + }); + + if (response.data && + response.data.length > 0 && + response.data.find(e => e.urlPattern === pathToLayer && e.serviceName === service && new Date(e.validUntil) > new Date())) { + return true; + } + + return false; +} \ No newline at end of file diff --git a/index.js b/index.js index 8d0a144..660fb2a 100644 --- a/index.js +++ b/index.js @@ -1,270 +1,89 @@ -const http = require('http'); +const express = require('express'); +const app = express(); +const port = 9000; const httpProxy = require('http-proxy'); -const axios = require('axios'); -const URL = require('url'); -const Redis = require("ioredis"); +const cookieParser = require('cookie-parser'); +const printError = require('./helpers/logs.helpers.js').printError; -// CONFIG -const redisSentinelHost = process.env.REDIS_SENTINEL_HOST; -const redisSentinelPort = process.env.REDIS_SENTINEL_PORT; -const redisGroupName = process.env.REDIS_GROUP_NAME; -const redisAuthorizedTTL = process.env.REDIS_AUTHORIZED_TTL; // In seconds -const redisUnauthorizedTTL = process.env.REDIS_UNAUTHORIZED_TTL; // In seconds -const elasticsearchUrl = process.env.ELASTICSEARCH_URL; -const technicalAccountUsername = process.env.TECHNICAL_ACCOUNT_USERNAME; -const technicalAccountPassword = process.env.TECHNICAL_ACCOUNT_PASSWORD; -const proxyHostTarget = process.env.PROXY_HOST_TARGET; -const ignKey = process.env.IGN_KEY; +// Parse the request headers in order to populate req.cookies +app.use(cookieParser()); -// Configuring the different proxy server -// Proxy IGN Ortho -var ingProxy = httpProxy.createProxyServer({ - changeOrigin: true, - target: 'https://wxs.ign.fr/' + ignKey + '/geoportail/r/wms/', -}); - -// Configuring the different proxy server -// Proxy WMS -var wmsProxy = httpProxy.createProxyServer({ - changeOrigin: true, - target: proxyHostTarget, - auth: `${technicalAccountUsername}:${technicalAccountPassword}`, +app.use(function(req, res, next) { + res.header("Access-Control-Allow-Origin", "*"); + next(); }); -// Configure and create an HTTP proxy server -var mvtProxy = httpProxy.createProxyServer({ - changeOrigin: true, - target: proxyHostTarget, - auth: `${technicalAccountUsername}:${technicalAccountPassword}`, -}); - -var mvtUnauthProxy = httpProxy.createProxyServer({ - changeOrigin: true, - target: proxyHostTarget, -}); +// CONFIG -ingProxy.on('proxyReq', function (proxyReq, req) { - req._proxyReq = proxyReq; -}); +app.locals.config = { + redisSentinelHost: process.env.REDIS_SENTINEL_HOST, + redisSentinelPort: process.env.REDIS_SENTINEL_PORT, + redisGroupName: process.env.REDIS_GROUP_NAME, + redisAuthorizedTTL: process.env.REDIS_AUTHORIZED_TTL, // In seconds + redisUnauthorizedTTL: process.env.REDIS_UNAUTHORIZED_TTL, // In seconds + elasticsearchUrl: process.env.ELASTICSEARCH_URL, + legacyAuthMiddlewareUrl: process.env.LEGACY_AUTH_MIDDLEWARE_URL, + technicalAccountUsername: process.env.TECHNICAL_ACCOUNT_USERNAME, + technicalAccountPassword: process.env.TECHNICAL_ACCOUNT_PASSWORD, + proxyHostTarget: process.env.PROXY_HOST_TARGET, + ignKey: process.env.IGN_KEY, +} -// keep a referece of the proxyRequest in the req object -wmsProxy.on('proxyReq', function (proxyReq, req) { - req._proxyReq = proxyReq; -}); -mvtProxy.on('proxyReq', function (proxyReq, req) { - req._proxyReq = proxyReq; -}); +// Proxies configuration -mvtUnauthProxy.on('proxyReq', function (proxyReq, req) { +// IGN Ortho +const IGNProxy = httpProxy.createProxyServer({ + changeOrigin: true, + target: 'https://wxs.ign.fr/' + app.locals.config.ignKey + '/geoportail/r/wms/', +}).on('proxyReq', (proxyReq, req) => { + // keep a reference of the proxyRequest in the req object req._proxyReq = proxyReq; -}); - -ingProxy.on('error', function (err, req, res) { - // If the client cancelled the request, abort the request to the upstream server - if (req.socket.destroyed && err.code === 'ECONNRESET') { - req._proxyReq.abort(); - } - return console.log(`ING Error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); -}); - -wmsProxy.on('error', function (err, req, res) { +}).on('error', (err, req, res) => { // If the client cancelled the request, abort the request to the upstream server if (req.socket.destroyed && err.code === 'ECONNRESET') { req._proxyReq.abort(); } - return console.log(`WMS Error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); + return console.log(`ING proxy error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); }); -mvtProxy.on('error', function (err, req, res) { +const AuthenticatedProxy = httpProxy.createProxyServer({ + changeOrigin: true, + target: app.locals.config.proxyHostTarget, + auth: `${app.locals.config.technicalAccountUsername}:${app.locals.config.technicalAccountPassword}`, +}).on('proxyReq', (proxyReq, req) => { + // keep a reference of the proxyRequest in the req object + req._proxyReq = proxyReq; +}).on('error', (err, req, res) => { // If the client cancelled the request, abort the request to the upstream server if (req.socket.destroyed && err.code === 'ECONNRESET') { req._proxyReq.abort(); } - return console.log(`MVT Error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); + return printError(`Authenticated proxy error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); }); -mvtUnauthProxy.on('error', function (err, req, res) { +var UnauthenticatedProxy = httpProxy.createProxyServer({ + changeOrigin: true, + target: app.locals.config.proxyHostTarget, +}).on('proxyReq', (proxyReq, req) => { + // keep a reference of the proxyRequest in the req object + req._proxyReq = proxyReq; +}).on('error', (err, req, res) => { // If the client cancelled the request, abort the request to the upstream server if (req.socket.destroyed && err.code === 'ECONNRESET') { req._proxyReq.abort(); } - return console.log(`MVT Unauthenticated Error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); + return printError(`Unauthenticated proxy Error, req.socket.destroyed: ${req.socket.destroyed}, ${err}`); }); -// Create an HTTP server -http.createServer(async function (req, res) { - - /******* IGN */ - if (req.url.includes('/ign')) { - req.headers['referer'] = 'grandlyon.com'; - ingProxy.web(req, res, {}); - return; - } - - /******************* WMS *****************************/ - if (req.url.includes('/wms')) { - wmsProxy.web(req, res, {}); - return; - } - - /******************* MVT *****************************/ - if (req.url.includes('/mvt')) { - // If no cookies then then we can't identify a user and directly proxy the request without credentials - if (req.headers["x-anonymous-consumer"]) { - mvtUnauthProxy.web(req, res, {}); - return; - } +app.locals.proxies = { + ign: IGNProxy, + authenticated: AuthenticatedProxy, + unauthenticated: UnauthenticatedProxy, +}; - // Read the requested layer from the url - const layer = getParameterValueFromUrl(req.url, 'LAYERS'); - - if (!layer) { - res.statusCode = 400; - res.end(); - return; - } - - const userRightsOnTheLayer = await getRedisValue(`${layer}-${req.headers['x-consumer-username']}`); - - if (userRightsOnTheLayer === 'true') { - mvtProxy.web(req, res, {}); - return; - } - - if (userRightsOnTheLayer === 'false') { - res.statusCode = 401; - res.end(); - return; - } - - const options = { - method: 'POST', - url: `${elasticsearchUrl}/_search?&request_cache=false`, - data: { - "from": 0, - "size": 1, - "_source": [ - "editorial-metadata.isSample", - "editorial-metadata.isOpenAccess" - ], - "query": { - "term": { - "metadata-fr.link.name": layer - } - } - }, - headers: { - cookie: req.headers.cookie, - } - }; - - let response; - - try { - response = await axios(options) - } catch (err) { - printError('Request to ES failed', err); - res.statusCode = 500; - res.end(); - return; - } - - - // If no results are found it means the specified dataset mvt layer doesn't exists - if (response.data.hits.hits < 1) { - printError('Request to ES', 'MVT not found'); - res.statusCode = 404; - res.end(); - return; - } - - const editorialMetadata = response.data.hits.hits[0]._source['editorial-metadata']; - - if (!editorialMetadata.isOpenAccess && editorialMetadata.isSample) { - setRedisValue(`${layer}-${req.headers['x-consumer-username']}`, false, redisUnauthorizedTTL); - res.statusCode = 401; - res.end(); - return; - } - - await setRedisValue(`${layer}-${req.headers['x-consumer-username']}`, true, redisAuthorizedTTL); - - mvtProxy.web(req, res, {}); - } - -}).listen(9000); - -// HELPERS - -function getParameterValueFromUrl(url, paramName) { - - const queryParams = URL.parse(url, true).query; - - return queryParams && queryParams[paramName] ? queryParams[paramName] : null; - -} - -async function setRedisValue(key, value, ttl) { - const redisClient = new Redis({ - sentinels: [{ - host: redisSentinelHost, - port: redisSentinelPort - },], - name: redisGroupName, - }); - - redisClient.on('error', (error) => { - printError('Redis client', error); - redisClient.disconnect(); - }); - - - // Set key value with expiration time in seconds - const res = await redisClient.set(key, value, 'EX', ttl).catch((error) => { - redisClient.disconnect(); - printError('Redis client', 'Couldn\'t set redis key/value (with ttl).'); - printError('Redis client', error); - return false; - }); - - - printLog('Redis client', 'Done setting key/value pair'); - redisClient.disconnect(); - - return res ? true : false; -} - -async function getRedisValue(key) { - const redisClient = new Redis({ - sentinels: [{ - host: redisSentinelHost, - port: redisSentinelPort - },], - name: redisGroupName, - }); - - redisClient.on('error', (error) => { - printError('Redis client', error); - redisClient.disconnect(); - }); - - const res = await redisClient.get(key).catch((error) => { - redisClient.disconnect(); - printError('Redis client', 'Couldn\'t get redis value.'); - printError('Redis client', error); - return false; - }); - - printLog(`Redis client`, `Value found ${res}`); - redisClient.disconnect(); - return res ? res : null; -} - -function printLog(context, value) { - console.log(`${new Date().toLocaleString("fr-FR")} [${context}] [log] `, value); -} +// ROUTES +app.use(require('./routes/index.js')); -function printError(context, error) { - console.error(`${new Date().toLocaleString("fr-FR")} [${context}] [error] `, error); -} \ No newline at end of file +// STARTING SERVER +app.listen(port, () => printError(`Proxy listening on port: ${port}`)); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1064f9d..0f390b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,23 @@ { - "name": "proxy-map-services", - "version": "1.0.1", + "name": "query-proxy", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, "axios": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", @@ -36,16 +50,80 @@ } } }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, "cluster-key-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, + "cookie-parser": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -59,11 +137,122 @@ "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "eventemitter3": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "follow-redirects": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", @@ -72,10 +261,27 @@ "debug": "^3.0.0" } }, - "http": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/http/-/http-0.0.0.tgz", - "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } }, "http-proxy": { "version": "1.18.0", @@ -87,6 +293,19 @@ "requires-port": "^1.0.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, "ioredis": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.14.1.tgz", @@ -113,6 +332,11 @@ } } }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -128,20 +352,96 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } }, "redis-commands": { "version": "1.5.0", @@ -166,19 +466,112 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "standard-as-callback": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" } } } diff --git a/package.json b/package.json index 9585de3..03304e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "proxy-map-services", - "version": "1.1.0", + "name": "query-proxy", + "version": "2.0.0", "description": "", "main": "index.js", "scripts": { @@ -11,10 +11,9 @@ "license": "GNU Affero General Public License v3.0", "dependencies": { "axios": "^0.19.0", - "cookie": "^0.4.0", - "http": "0.0.0", + "cookie-parser": "^1.4.5", + "express": "^4.17.1", "http-proxy": "^1.18.0", - "ioredis": "^4.14.1", - "url": "^0.11.0" + "ioredis": "^4.14.1" } } diff --git a/routes/download.routes.js b/routes/download.routes.js new file mode 100644 index 0000000..e0e98d2 --- /dev/null +++ b/routes/download.routes.js @@ -0,0 +1,108 @@ +var router = require('express').Router(); +const requestProcessor = require('../helpers/request-processor.helper.js').requestProcessor; +const printError = require('../helpers/logs.helpers.js').printError; + +router.get('/wms/:repo*', async (req, res) => { + + const layer = req.query.layers || req.query.LAYERS; + + try { + + return await requestProcessor(req, res, layer, 'wms'); + + } catch (err) { + printError(`/wms/${req.params.repo}?layers=${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/wfs/:repo*', async (req, res) => { + + const layer = req.query.typename || req.query.TYPENAME; + + try { + + return await requestProcessor(req, res, layer, 'wfs'); + + } catch (err) { + printError(`/wfs/${req.params.repo}?typename=${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/ws/:repo/:layer*', async (req, res) => { + + const layer = req.params.layer; + + try { + + return await requestProcessor(req, res, layer, 'ws'); + + } catch (err) { + printError(`/ws/${req.params.repo}/${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/kml/:repo*', async (req, res) => { + + const layer = req.query.typename || req.query.TYPENAME; + + try { + + return await requestProcessor(req, res, layer, 'kml'); + + } catch (err) { + printError(`/kml/${req.params.repo}?typename=${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/wcs/:repo*', async (req, res) => { + + const layer = req.query.identifiers || req.query.IDENTIFIERS; + + try { + + return await requestProcessor(req, res, layer, 'wcs'); + + } catch (err) { + printError(`/wcs/${req.params.repo}?identifiers=${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/files/:repo/:layer*', async (req, res) => { + + const layer = req.params.layer; + + try { + + return await requestProcessor(req, res, layer, 'files', req.path); + + } catch (err) { + printError(`/files/${req.params.repo}/${layer}`, err); + res.status(err.status).send(err); + return; + } +}); + +router.get('/catalogue*', async (req, res) => { + + try { + + req.app.locals.proxies.unauthenticated.web(req, res, {}); + + } catch (err) { + printError(`/catalogue`, err); + res.status(err.status).send(err); + return; + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..80eee0e --- /dev/null +++ b/routes/index.js @@ -0,0 +1,6 @@ +var router = require('express').Router(); + +router.use('/map', require('./map.routes.js')); +router.use('/download', require('./download.routes.js')); + +module.exports = router; \ No newline at end of file diff --git a/routes/map.routes.js b/routes/map.routes.js new file mode 100644 index 0000000..5b60fae --- /dev/null +++ b/routes/map.routes.js @@ -0,0 +1,76 @@ +const router = require('express').Router(); +const getRedisValue = require('../helpers/redis.helpers.js').getRedisValue; +const setRedisValue = require('../helpers/redis.helpers.js').setRedisValue; +const getDatasetInfoFromES = require('../helpers/elasticsearch.helpers.js').getDatasetInfoFromES; + +router.get('/ign', (req, res, next) => { + req.headers['referer'] = 'grandlyon.com'; + req.app.locals.proxies.ign.web(req, res, {}); + return; +}); + +router.get('/wms*', (req, res, next) => { + req.app.locals.proxies.authenticated.web(req, res, {}); + return; +}); + +router.get('/mvt*', async (req, res, next) => { + + // If no cookies then then we can't identify a user and directly proxy the request without credentials + if (req.headers["x-anonymous-consumer"]) { + req.app.locals.proxies.unauthenticated.web(req, res, {}); + return; + } + + // Read the requested layer from the url + const layer = req.query.LAYERS; + + if (!layer) { + res.status(400).send({err: "Bad request, missing LAYERS parameter."}) + return; + } + + const userRightsOnTheLayer = await getRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `map-mvt-${layer}-${req.headers['x-consumer-username']}`); + + if (userRightsOnTheLayer === 'true') { + req.app.locals.proxies.authenticated.web(req, res, {}); + return; + } + + if (userRightsOnTheLayer === 'false') { + res.status(401).send({err: 'Unauthenticated, you don\'t have access to this layer'}); + return; + } + + let source; + + try { + source = await getDatasetInfoFromES( + req.app.locals.config.elasticsearchUrl, + layer, + [ + "editorial-metadata.isSample", + "editorial-metadata.isOpenAccess" + ], + req.headers.cookie, + ); + } catch(err) { + printError('/mvt', err); + res.status(err.status).send(err); + return; + } + + const editorialMetadata = source['editorial-metadata']; + + if (!editorialMetadata.isOpenAccess && editorialMetadata.isSample) { + await setRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `map-mvt-${layer}-${req.headers['x-consumer-username']}`, false, req.app.locals.config.redisUnauthorizedTTL); + res.status(401).send(); + return; + } + + await setRedisValue(req.app.locals.config.redisSentinelHost, req.app.locals.config.redisSentinelPort, req.app.locals.config.redisGroupName, `map-mvt-${layer}-${req.headers['x-consumer-username']}`, true, req.app.locals.config.redisAuthorizedTTL); + + req.app.locals.proxies.authenticated.web(req, res, {}); +}); + +module.exports = router; \ No newline at end of file diff --git a/template.env b/template.env index 01d8d57..746a801 100644 --- a/template.env +++ b/template.env @@ -1,9 +1,10 @@ TAG=<tag of the image to be used> -PROXY_MAP_SERVICES_BIND_PORT=<proxy port> +PROXY_QUERY_BIND_PORT=<proxy port> TECHNICAL_ACCOUNT_PASSWORD=<username of the technical account which has access to all wms and mvt> TECHNICAL_ACCOUNT_USERNAME=<password of the technical account> ELASTICSEARCH_URL=<url of the elasticsearch instance> PROXY_HOST_TARGET=<url of the map services> +LEGACY_AUTH_MIDDLEWARE_URL=<url of the legacy auth middleware> REDIS_MASTER_HOST=<host of the redis master> REDIS_SENTINEL_PORT=<port of the redis sentinel> REDIS_SENTINEL_HOST=<host of the redis sentinel> -- GitLab