From 7fa2e1aa0ccc49efac2933e83eca84a45bc53a62 Mon Sep 17 00:00:00 2001 From: sedum Date: Fri, 13 Jan 2023 22:28:38 +0100 Subject: [PATCH 1/6] init api rate-limit on geolocation api routes --- package.json | 1 + server/api/controller/geolocation.js | 38 ++++++++++++++++++++++++++++ server/api/index.js | 6 ++--- yarn.lock | 7 +++-- 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 server/api/controller/geolocation.js diff --git a/package.json b/package.json index e2c0d2fc..437e4b93 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "dompurify": "^2.4.1", "email-templates": "^10.0.1", "express": "^4.18.1", + "express-rate-limit": "^6.7.0", "http-signature": "^1.3.6", "https-proxy-agent": "^5.0.1", "ical.js": "^1.5.0", diff --git a/server/api/controller/geolocation.js b/server/api/controller/geolocation.js new file mode 100644 index 00000000..fd0aa220 --- /dev/null +++ b/server/api/controller/geolocation.js @@ -0,0 +1,38 @@ +const rateLimit = require('express-rate-limit'); +const log = require('../../log') +let curReq + +const geolocationController = { + rateLimiter: rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + }), + + /** + * Limit api usage + * From https://operations.osmfoundation.org/policies/nominatim/ + * [Requirements] No heavy uses (an absolute maximum of 1 request per second). + * [Websites and Apps] Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. + */ + apiLimit (req, res, next) { + prevReq = curReq + curReq = Date.now() + deltaTime = (curReq - prevReq) + + if (typeof prevReq === 'undefined' || deltaTime > 1000) { + geolocationController.rateLimiter(req, res, next) + } else { + log.warn('More than 1 request per second to geolocation api come from ' + req.ip) + + setTimeout(() => { + geolocationController.rateLimiter(req, res, next) + }, 1000 - deltaTime) + } + + } + +} + +module.exports = geolocationController \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index 49d0e61d..32e15198 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -19,6 +19,7 @@ const resourceController = require('./controller/resource') const oauthController = require('./controller/oauth') const announceController = require('./controller/announce') const pluginController = require('./controller/plugins') +const geolocationController = require('./controller/geolocation') const helpers = require('../helpers') const storage = require('./storage') @@ -65,7 +66,6 @@ module.exports = () => { api.get('/ping', (_req, res) => res.sendStatus(200)) api.get('/user', isAuth, (req, res) => res.json(req.user)) - api.post('/user/recover', userController.forgotPassword) api.post('/user/check_recover_code', userController.checkRecoverCode) api.post('/user/recover_password', userController.updatePasswordWithRecoverCode) @@ -173,8 +173,8 @@ module.exports = () => { api.put('/place', isAdmin, placeController.updatePlace) // - GEOCODING - api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, placeController._nominatim) - api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, placeController._photon) + api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geolocationController.apiLimit, placeController._nominatim) + api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geolocationController.apiLimit, placeController._photon) // - TAGS api.get('/tags', isAdmin, tagController.getAll) diff --git a/yarn.lock b/yarn.lock index 7de9e8e5..2445ccf5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5160,6 +5160,11 @@ expect@^29.3.1: jest-message-util "^29.3.1" jest-util "^29.3.1" +express-rate-limit@^6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.7.0.tgz#6aa8a1bd63dfe79702267b3af1161a93afc1d3c2" + integrity sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA== + express@^4.18.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -12636,10 +12641,8 @@ watchpack@^1.7.4: resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^3.4.1" graceful-fs "^4.1.2" neo-async "^2.5.0" - watchpack-chokidar2 "^2.0.1" optionalDependencies: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" From ebb3fcbd93b4b5290684c8805fc7a1f4a596af7b Mon Sep 17 00:00:00 2001 From: sedum Date: Sat, 14 Jan 2023 14:53:51 +0100 Subject: [PATCH 2/6] geolocation api rate-limit: add a delay of at least 1s between requests --- server/api/controller/geolocation.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/server/api/controller/geolocation.js b/server/api/controller/geolocation.js index fd0aa220..9ff07ead 100644 --- a/server/api/controller/geolocation.js +++ b/server/api/controller/geolocation.js @@ -1,6 +1,6 @@ const rateLimit = require('express-rate-limit'); const log = require('../../log') -let curReq +let d // departure time const geolocationController = { rateLimiter: rateLimit({ @@ -17,18 +17,26 @@ const geolocationController = { * [Websites and Apps] Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. */ apiLimit (req, res, next) { - prevReq = curReq - curReq = Date.now() - deltaTime = (curReq - prevReq) + let dprev = d // departure time of previous + let a = Date.now() // arrival time - if (typeof prevReq === 'undefined' || deltaTime > 1000) { - geolocationController.rateLimiter(req, res, next) - } else { - log.warn('More than 1 request per second to geolocation api come from ' + req.ip) + if (typeof dprev !== 'undefined') { + d = dprev + 1000 - setTimeout(() => { + if (a > d) { + d = a + 10 geolocationController.rateLimiter(req, res, next) - }, 1000 - deltaTime) + } else { + let wait = d - a + log.warn('More than 1 request per second to geolocation api. This from ' + req.ip) + + setTimeout(() => { + geolocationController.rateLimiter(req, res, next) + }, wait) + } + } else { + d = a + 10 // add 10ms + geolocationController.rateLimiter(req, res, next) } } From 6ee96fb07c1357cdeee4df3d3bfc3b53d221d7a9 Mon Sep 17 00:00:00 2001 From: sedum Date: Thu, 19 Jan 2023 01:29:24 +0100 Subject: [PATCH 3/6] geolocation api rate-limit: improve the delay mechanism to be sure to don't hit provider more than 1 time/s, add memory-cache to save response data. --- package.json | 1 + server/api/controller/geolocation.js | 128 ++++++++++++++++++++++----- server/api/controller/place.js | 44 +-------- server/api/index.js | 4 +- yarn.lock | 5 ++ 5 files changed, 115 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 437e4b93..7046e50a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "linkifyjs": "4.0.2", "lodash": "^4.17.21", "mariadb": "^3.0.1", + "memory-cache": "^0.2.0", "microformat-node": "^2.0.1", "minify-css-string": "^1.0.0", "mkdirp": "^1.0.4", diff --git a/server/api/controller/geolocation.js b/server/api/controller/geolocation.js index 9ff07ead..0233e919 100644 --- a/server/api/controller/geolocation.js +++ b/server/api/controller/geolocation.js @@ -1,9 +1,18 @@ const rateLimit = require('express-rate-limit'); const log = require('../../log') -let d // departure time +const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search' +const PHOTON_URL = 'https://photon.komoot.io/api/' +const axios = require('axios') +const { version } = require('../../../package.json') +const cache = require('memory-cache'); +let d = 0 // departure time +let h = 0 // hit geocoding provider time (aka Latency) const geolocationController = { - rateLimiter: rateLimit({ + /** + * TODO: replace/merge with a general 'instance rate-limiter' or 'instance api-related rate-limiter' when this will be defined + */ + instanceApiRateLimiter: rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers @@ -14,33 +23,108 @@ const geolocationController = { * Limit api usage * From https://operations.osmfoundation.org/policies/nominatim/ * [Requirements] No heavy uses (an absolute maximum of 1 request per second). - * [Websites and Apps] Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. + * [Websites and Apps] + * - Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. + * - If at all possible, set up a proxy and also enable caching of requests. */ - apiLimit (req, res, next) { - let dprev = d // departure time of previous - let a = Date.now() // arrival time + providerRateLimit (req, res, next) { + let a = Date.now(); // arrival time + let dprev = d + d = dprev + 1000 + h - if (typeof dprev !== 'undefined') { - d = dprev + 1000 + console.log('a: ' + a) + console.log('dprev: ' + dprev) + console.log('d: ' + d) - if (a > d) { - d = a + 10 - geolocationController.rateLimiter(req, res, next) - } else { - let wait = d - a - log.warn('More than 1 request per second to geolocation api. This from ' + req.ip) - - setTimeout(() => { - geolocationController.rateLimiter(req, res, next) - }, wait) + // if the same request was already cached skip the delay mechanism + if (cache.get(req.params.place_details)) { + if (a < d) { + log.warn('More than 1 request per second to geolocation api. This from ' + req.ip + ' . The response data is served from memory-cache') } + // reset departure time because there is no need to ask provider + d = dprev + return next() + } + + if (d === 0 || a > d) { + // no-queue or old-queue + console.log('No queue or Old queue') + // arrival time + 10ms estimated computing time + d = a + 10 + next() } else { - d = a + 10 // add 10ms - geolocationController.rateLimiter(req, res, next) + // fresh queue + console.log('Fresh queue') + let wait = d - a + console.log('Waiting '+ wait) + log.warn('More than 1 request per second to geolocation api. This from ' + req.ip + ' . Applying ToS padding before asking to provider. The response data is now cached.') + + setTimeout(() => { + next() + }, wait) } - } - + }, + + async _nominatim (req, res) { + const details = req.params.place_details + const countrycodes = res.locals.settings.geocoding_countrycodes || [] + const geocoding_provider = res.locals.settings.geocoding_provider || NOMINATIM_URL + // ?limit=3&format=json&namedetails=1&addressdetails=1&q= + + const ret = await axios.get(`${geocoding_provider}`, { + params: { + countrycodes: countrycodes.join(','), + q: details, + limit: 3, + format: 'json', + addressdetails: 1, + namedetails: 1, + }, + headers: { 'User-Agent': `gancio ${version}` } + }) + + return res.json(ret.data) + + }, + + async _photon (req, res) { + const details = req.params.place_details + const geocoding_provider = res.locals.settings.geocoding_provider || PHOTON_URL + + if (cache.get(details)) { + console.log('Retrieving data from cache') + const ret = { + data: await cache.get(details) + } + return res.json(ret.data) + } else { + let RTTstart = Date.now() + + console.log('Asking Provider: ' + RTTstart) + const ret = await axios.get(`${geocoding_provider}`, { + params: { + q: details, + limit: 3, + }, + headers: { 'User-Agent': `gancio ${version}` } + }) + + if (ret) { + let RTTend = Date.now() + console.log('Response arrived: ' + RTTend) + // Save the hit time (aka Latency) + h = (RTTend - RTTstart) / 2 + console.log('Saving latency h: ' + h) + } + + console.log('Caching results') + cache.put(details, ret.data); + return res.json(ret.data) + } + + }, + } module.exports = geolocationController \ No newline at end of file diff --git a/server/api/controller/place.js b/server/api/controller/place.js index 8fd1aa3b..47f71b34 100644 --- a/server/api/controller/place.js +++ b/server/api/controller/place.js @@ -7,9 +7,6 @@ const { version } = require('../../../package.json') const log = require('../../log') const { Op, where, col, fn, cast } = require('sequelize') -const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search' -const PHOTON_URL = 'https://photon.komoot.io/api/' -const axios = require('axios') module.exports = { @@ -75,45 +72,6 @@ module.exports = { // TOFIX: don't know why limit does not work return res.json(places.slice(0, 10)) - }, - - async _nominatim (req, res) { - const details = req.params.place_details - const countrycodes = res.locals.settings.geocoding_countrycodes || [] - const geocoding_provider = res.locals.settings.geocoding_provider || NOMINATIM_URL - // ?limit=3&format=json&namedetails=1&addressdetails=1&q= - - const ret = await axios.get(`${geocoding_provider}`, { - params: { - countrycodes: countrycodes.join(','), - q: details, - limit: 3, - format: 'json', - addressdetails: 1, - namedetails: 1, - }, - headers: { 'User-Agent': `gancio ${version}` } - }) - - return res.json(ret.data) - - }, - - async _photon (req, res) { - const details = req.params.place_details - const geocoding_provider = res.locals.settings.geocoding_provider || PHOTON_URL - - const ret = await axios.get(`${geocoding_provider}`, { - params: { - q: details, - limit: 3, - }, - headers: { 'User-Agent': `gancio ${version}` } - }) - - // console.log(ret) - return res.json(ret.data) - - }, + } } diff --git a/server/api/index.js b/server/api/index.js index 32e15198..a2cecb4f 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -173,8 +173,8 @@ module.exports = () => { api.put('/place', isAdmin, placeController.updatePlace) // - GEOCODING - api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geolocationController.apiLimit, placeController._nominatim) - api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geolocationController.apiLimit, placeController._photon) + api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geolocationController.instanceApiRateLimiter, geolocationController.providerRateLimit, geolocationController._nominatim) + api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geolocationController.instanceApiRateLimiter, geolocationController.providerRateLimit, geolocationController._photon) // - TAGS api.get('/tags', isAdmin, tagController.getAll) diff --git a/yarn.lock b/yarn.lock index 2445ccf5..a5d03c8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7950,6 +7950,11 @@ memoizee@^0.4.15: next-tick "^1.1.0" timers-ext "^0.1.7" +memory-cache@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" + integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" From cc4bb69667ea9ab905532fd5856ac10e7312187e Mon Sep 17 00:00:00 2001 From: sedum Date: Thu, 19 Jan 2023 12:06:23 +0100 Subject: [PATCH 4/6] geolocation api rate-limit: add per-provider prefix to cached response, refactor geocoding provider code --- server/api/controller/geocoding.js | 135 +++++++++++++++++++++++++ server/api/controller/geolocation.js | 130 ------------------------ server/api/index.js | 6 +- server/services/geocoding/nominatim.js | 23 +++++ server/services/geocoding/photon.js | 18 ++++ 5 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 server/api/controller/geocoding.js delete mode 100644 server/api/controller/geolocation.js create mode 100644 server/services/geocoding/nominatim.js create mode 100644 server/services/geocoding/photon.js diff --git a/server/api/controller/geocoding.js b/server/api/controller/geocoding.js new file mode 100644 index 00000000..5e382640 --- /dev/null +++ b/server/api/controller/geocoding.js @@ -0,0 +1,135 @@ +const rateLimit = require('express-rate-limit'); +const log = require('../../log') +const nominatim = require('../../services/geocoding/nominatim') +const photon = require('../../services/geocoding/photon') +const axios = require('axios') +const { version } = require('../../../package.json') +const cache = require('memory-cache'); +let d = 0 // departure time +let h = 0 // hit geocoding provider time (aka Latency) + +const geocodingController = { + /** + * TODO: replace/merge with a general 'instance rate-limiter' or 'instance api-related rate-limiter' when this will be defined + */ + instanceApiRateLimiter: rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + }), + + /** + * Limit provider api usage. + * From https://operations.osmfoundation.org/policies/nominatim/ + * [Requirements] No heavy uses (an absolute maximum of 1 request per second). + * [Websites and Apps] + * - Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. + * - If at all possible, set up a proxy and also enable caching of requests. + */ + providerRateLimit (req, res, next, providerCommonName) { + let a = Date.now(); // arrival time + let dprev = d + d = dprev + 1000 + h + + console.log('a: ' + a) + console.log('dprev: ' + dprev) + console.log('d: ' + d) + + // if the same request was already cached skip the delay mechanism + if (cache.get(providerCommonName + '_' + req.params.place_details)) { + if (a < d) { + log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . The response data is served from memory-cache') + } + console.log('yet cached') + // reset departure time because there is no need to ask provider + d = dprev + return next() + } + + if (d === 0 || a > d) { + // no-queue or old-queue + console.log('No queue or Old queue') + // arrival time + 10ms estimated computing time + d = a + 10 + next() + } else { + // fresh queue + console.log('Fresh queue') + let wait = d - a + console.log('Waiting '+ wait) + log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . Applying ToS padding before asking to provider. The response data is now cached.') + + setTimeout(() => { + next() + }, wait) + } + + }, + + async nominatimRateLimit(req, res, next) { + geocodingController.providerRateLimit(req, res, next, nominatim.commonName) + }, + + async photonRateLimit(req, res, next) { + geocodingController.providerRateLimit(req, res, next, photon.commonName) + }, + + async checkInCache (req, res, details, providerCommonName) { + if (cache.get(providerCommonName + '_' + details)) { + const ret = await cache.get(providerCommonName + '_' + details) + return ret + } else { + console.log('Not in cache') + return + } + }, + + async queryProvider(req, res, details, provider) { + let RTTstart = Date.now() + console.log('Asking Provider: ' + RTTstart) + + const ret = await axios.get(`${provider.endpoint(req, res)}`, { + params: provider.getParams(req, res), + headers: { 'User-Agent': `gancio ${version}` } + }) + + if (ret) { + let RTTend = Date.now() + console.log('Asking Provider: ' + RTTend) + // Save the hit time (aka Latency) + console.log('Saving latency h: ' + h) + h = (RTTend - RTTstart) / 2 + } + + // Cache the response data + cache.put(provider.commonName + '_' + details, ret.data, 1000 * 60 * 60 * 24); + console.log(cache.keys()) + console.log(cache.exportJson()) + return ret.data + }, + + + async _nominatim (req, res) { + const details = req.params.place_details + + const ret = await geocodingController.checkInCache(req, res, details, nominatim.commonName) || + await geocodingController.queryProvider(req, res, details, nominatim) + + return res.json(ret) + + }, + + async _photon (req, res) { + const details = req.params.place_details + + const ret = await geocodingController.checkInCache(req, res, details, photon.commonName) || + await geocodingController.queryProvider(req, res, details, photon) + + return res.json(ret) + + }, + +} + +module.exports = geocodingController \ No newline at end of file diff --git a/server/api/controller/geolocation.js b/server/api/controller/geolocation.js deleted file mode 100644 index 0233e919..00000000 --- a/server/api/controller/geolocation.js +++ /dev/null @@ -1,130 +0,0 @@ -const rateLimit = require('express-rate-limit'); -const log = require('../../log') -const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search' -const PHOTON_URL = 'https://photon.komoot.io/api/' -const axios = require('axios') -const { version } = require('../../../package.json') -const cache = require('memory-cache'); -let d = 0 // departure time -let h = 0 // hit geocoding provider time (aka Latency) - -const geolocationController = { - /** - * TODO: replace/merge with a general 'instance rate-limiter' or 'instance api-related rate-limiter' when this will be defined - */ - instanceApiRateLimiter: rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers - }), - - /** - * Limit api usage - * From https://operations.osmfoundation.org/policies/nominatim/ - * [Requirements] No heavy uses (an absolute maximum of 1 request per second). - * [Websites and Apps] - * - Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. - * - If at all possible, set up a proxy and also enable caching of requests. - */ - providerRateLimit (req, res, next) { - let a = Date.now(); // arrival time - let dprev = d - d = dprev + 1000 + h - - console.log('a: ' + a) - console.log('dprev: ' + dprev) - console.log('d: ' + d) - - // if the same request was already cached skip the delay mechanism - if (cache.get(req.params.place_details)) { - if (a < d) { - log.warn('More than 1 request per second to geolocation api. This from ' + req.ip + ' . The response data is served from memory-cache') - } - // reset departure time because there is no need to ask provider - d = dprev - return next() - } - - if (d === 0 || a > d) { - // no-queue or old-queue - console.log('No queue or Old queue') - // arrival time + 10ms estimated computing time - d = a + 10 - next() - } else { - // fresh queue - console.log('Fresh queue') - let wait = d - a - console.log('Waiting '+ wait) - log.warn('More than 1 request per second to geolocation api. This from ' + req.ip + ' . Applying ToS padding before asking to provider. The response data is now cached.') - - setTimeout(() => { - next() - }, wait) - } - - }, - - async _nominatim (req, res) { - const details = req.params.place_details - const countrycodes = res.locals.settings.geocoding_countrycodes || [] - const geocoding_provider = res.locals.settings.geocoding_provider || NOMINATIM_URL - // ?limit=3&format=json&namedetails=1&addressdetails=1&q= - - const ret = await axios.get(`${geocoding_provider}`, { - params: { - countrycodes: countrycodes.join(','), - q: details, - limit: 3, - format: 'json', - addressdetails: 1, - namedetails: 1, - }, - headers: { 'User-Agent': `gancio ${version}` } - }) - - return res.json(ret.data) - - }, - - async _photon (req, res) { - const details = req.params.place_details - const geocoding_provider = res.locals.settings.geocoding_provider || PHOTON_URL - - if (cache.get(details)) { - console.log('Retrieving data from cache') - const ret = { - data: await cache.get(details) - } - return res.json(ret.data) - } else { - let RTTstart = Date.now() - - console.log('Asking Provider: ' + RTTstart) - const ret = await axios.get(`${geocoding_provider}`, { - params: { - q: details, - limit: 3, - }, - headers: { 'User-Agent': `gancio ${version}` } - }) - - if (ret) { - let RTTend = Date.now() - console.log('Response arrived: ' + RTTend) - // Save the hit time (aka Latency) - h = (RTTend - RTTstart) / 2 - console.log('Saving latency h: ' + h) - } - - console.log('Caching results') - cache.put(details, ret.data); - return res.json(ret.data) - } - - }, - -} - -module.exports = geolocationController \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index a2cecb4f..1dfae154 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -19,7 +19,7 @@ const resourceController = require('./controller/resource') const oauthController = require('./controller/oauth') const announceController = require('./controller/announce') const pluginController = require('./controller/plugins') -const geolocationController = require('./controller/geolocation') +const geocodingController = require('./controller/geocoding') const helpers = require('../helpers') const storage = require('./storage') @@ -173,8 +173,8 @@ module.exports = () => { api.put('/place', isAdmin, placeController.updatePlace) // - GEOCODING - api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geolocationController.instanceApiRateLimiter, geolocationController.providerRateLimit, geolocationController._nominatim) - api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geolocationController.instanceApiRateLimiter, geolocationController.providerRateLimit, geolocationController._photon) + api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geocodingController.instanceApiRateLimiter, geocodingController.nominatimRateLimit, geocodingController._nominatim) + api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geocodingController.instanceApiRateLimiter, geocodingController.photonRateLimit, geocodingController._photon) // - TAGS api.get('/tags', isAdmin, tagController.getAll) diff --git a/server/services/geocoding/nominatim.js b/server/services/geocoding/nominatim.js new file mode 100644 index 00000000..8b55153a --- /dev/null +++ b/server/services/geocoding/nominatim.js @@ -0,0 +1,23 @@ + +module.exports = { + commonName: 'Nominatim', + DEFAULT_ENDPOINT: 'https://nominatim.openstreetmap.org/search', + endpoint: (req, res) => { + return res.locals.settings.geocoding_provider || this.DEFAULT_ENDPOINT + }, + + getParams (req, res) { + const countrycodes = res.locals.settings.geocoding_countrycodes || [] + const details = req.params.place_details + + return { + countrycodes: countrycodes.join(','), + q: details, + limit: 3, + format: 'json', + addressdetails: 1, + namedetails: 1, + } + }, + +} \ No newline at end of file diff --git a/server/services/geocoding/photon.js b/server/services/geocoding/photon.js new file mode 100644 index 00000000..29a4dfda --- /dev/null +++ b/server/services/geocoding/photon.js @@ -0,0 +1,18 @@ + +module.exports = { + commonName: 'Photon', + DEFAULT_ENDPOINT: 'https://photon.komoot.io/api/', + endpoint: (req, res) => { + return res.locals.settings.geocoding_provider || this.DEFAULT_ENDPOINT + }, + + getParams (req, res) { + const details = req.params.place_details + + return { + q: details, + limit: 3, + } + } + +} \ No newline at end of file From c50d369de9132b14da75a4a6312a5b9493c8cb7d Mon Sep 17 00:00:00 2001 From: sedum Date: Thu, 19 Jan 2023 12:20:52 +0100 Subject: [PATCH 5/6] geolocation api rate-limit: commented debug messages --- server/api/controller/geocoding.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/server/api/controller/geocoding.js b/server/api/controller/geocoding.js index 5e382640..c7b9fd9a 100644 --- a/server/api/controller/geocoding.js +++ b/server/api/controller/geocoding.js @@ -32,16 +32,15 @@ const geocodingController = { let dprev = d d = dprev + 1000 + h - console.log('a: ' + a) - console.log('dprev: ' + dprev) - console.log('d: ' + d) + // console.log('a: ' + a) + // console.log('dprev: ' + dprev) + // console.log('d: ' + d) // if the same request was already cached skip the delay mechanism if (cache.get(providerCommonName + '_' + req.params.place_details)) { if (a < d) { - log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . The response data is served from memory-cache') + log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . The response data is served from memory-cache.') } - console.log('yet cached') // reset departure time because there is no need to ask provider d = dprev return next() @@ -49,15 +48,15 @@ const geocodingController = { if (d === 0 || a > d) { // no-queue or old-queue - console.log('No queue or Old queue') + // console.log('No queue or Old queue') // arrival time + 10ms estimated computing time d = a + 10 next() } else { // fresh queue - console.log('Fresh queue') + // console.log('Fresh queue') let wait = d - a - console.log('Waiting '+ wait) + // console.log('Waiting '+ wait) log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . Applying ToS padding before asking to provider. The response data is now cached.') setTimeout(() => { @@ -80,14 +79,14 @@ const geocodingController = { const ret = await cache.get(providerCommonName + '_' + details) return ret } else { - console.log('Not in cache') + // console.log('Not in cache') return } }, async queryProvider(req, res, details, provider) { let RTTstart = Date.now() - console.log('Asking Provider: ' + RTTstart) + // console.log('Asking Provider: ' + RTTstart) const ret = await axios.get(`${provider.endpoint(req, res)}`, { params: provider.getParams(req, res), @@ -96,16 +95,16 @@ const geocodingController = { if (ret) { let RTTend = Date.now() - console.log('Asking Provider: ' + RTTend) + // console.log('Asking Provider: ' + RTTend) // Save the hit time (aka Latency) - console.log('Saving latency h: ' + h) + // console.log('Saving latency h: ' + h) h = (RTTend - RTTstart) / 2 } // Cache the response data cache.put(provider.commonName + '_' + details, ret.data, 1000 * 60 * 60 * 24); - console.log(cache.keys()) - console.log(cache.exportJson()) + // console.log(cache.keys()) + // console.log(cache.exportJson()) return ret.data }, From ad3ace1e7e77a3c867b00a9c7363c11d1c651a83 Mon Sep 17 00:00:00 2001 From: sedum Date: Sat, 21 Jan 2023 01:18:16 +0100 Subject: [PATCH 6/6] geolocation api rate-limit: split cache instance, one for each provider --- server/api/controller/geocoding.js | 22 ++++++++++------------ server/services/geocoding/nominatim.js | 11 ++++++++--- server/services/geocoding/photon.js | 11 ++++++++--- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/server/api/controller/geocoding.js b/server/api/controller/geocoding.js index c7b9fd9a..a86b1ca3 100644 --- a/server/api/controller/geocoding.js +++ b/server/api/controller/geocoding.js @@ -4,7 +4,6 @@ const nominatim = require('../../services/geocoding/nominatim') const photon = require('../../services/geocoding/photon') const axios = require('axios') const { version } = require('../../../package.json') -const cache = require('memory-cache'); let d = 0 // departure time let h = 0 // hit geocoding provider time (aka Latency) @@ -27,7 +26,7 @@ const geocodingController = { * - Note that the usage limits above apply per website/application: the sum of traffic by all your users should not exceed the limits. * - If at all possible, set up a proxy and also enable caching of requests. */ - providerRateLimit (req, res, next, providerCommonName) { + providerRateLimit (req, res, next, providerCache) { let a = Date.now(); // arrival time let dprev = d d = dprev + 1000 + h @@ -37,7 +36,7 @@ const geocodingController = { // console.log('d: ' + d) // if the same request was already cached skip the delay mechanism - if (cache.get(providerCommonName + '_' + req.params.place_details)) { + if (providerCache.get(req.params.place_details)) { if (a < d) { log.warn('More than 1 request per second to geocoding api. This from ' + req.ip + ' . The response data is served from memory-cache.') } @@ -67,19 +66,18 @@ const geocodingController = { }, async nominatimRateLimit(req, res, next) { - geocodingController.providerRateLimit(req, res, next, nominatim.commonName) + geocodingController.providerRateLimit(req, res, next, nominatim.cache) }, async photonRateLimit(req, res, next) { - geocodingController.providerRateLimit(req, res, next, photon.commonName) + geocodingController.providerRateLimit(req, res, next, photon.cache) }, - async checkInCache (req, res, details, providerCommonName) { - if (cache.get(providerCommonName + '_' + details)) { - const ret = await cache.get(providerCommonName + '_' + details) + async checkInCache (req, res, details, providerCache) { + const ret = await providerCache.get(details) + if (ret) { return ret } else { - // console.log('Not in cache') return } }, @@ -102,7 +100,7 @@ const geocodingController = { } // Cache the response data - cache.put(provider.commonName + '_' + details, ret.data, 1000 * 60 * 60 * 24); + provider.cache.put(details, ret.data, 1000 * 60 * 60 * 24); // console.log(cache.keys()) // console.log(cache.exportJson()) return ret.data @@ -112,7 +110,7 @@ const geocodingController = { async _nominatim (req, res) { const details = req.params.place_details - const ret = await geocodingController.checkInCache(req, res, details, nominatim.commonName) || + const ret = await geocodingController.checkInCache(req, res, details, nominatim.cache) || await geocodingController.queryProvider(req, res, details, nominatim) return res.json(ret) @@ -122,7 +120,7 @@ const geocodingController = { async _photon (req, res) { const details = req.params.place_details - const ret = await geocodingController.checkInCache(req, res, details, photon.commonName) || + const ret = await geocodingController.checkInCache(req, res, details, photon.cache) || await geocodingController.queryProvider(req, res, details, photon) return res.json(ret) diff --git a/server/services/geocoding/nominatim.js b/server/services/geocoding/nominatim.js index 8b55153a..9e8b09fd 100644 --- a/server/services/geocoding/nominatim.js +++ b/server/services/geocoding/nominatim.js @@ -1,10 +1,13 @@ +const cache = require('memory-cache') +const providerCache = new cache.Cache() -module.exports = { +const nominatim = { commonName: 'Nominatim', DEFAULT_ENDPOINT: 'https://nominatim.openstreetmap.org/search', endpoint: (req, res) => { - return res.locals.settings.geocoding_provider || this.DEFAULT_ENDPOINT + return res.locals.settings.geocoding_provider || nominatim.DEFAULT_ENDPOINT }, + cache: providerCache, getParams (req, res) { const countrycodes = res.locals.settings.geocoding_countrycodes || [] @@ -20,4 +23,6 @@ module.exports = { } }, -} \ No newline at end of file +} + +module.exports = nominatim \ No newline at end of file diff --git a/server/services/geocoding/photon.js b/server/services/geocoding/photon.js index 29a4dfda..4eced7db 100644 --- a/server/services/geocoding/photon.js +++ b/server/services/geocoding/photon.js @@ -1,10 +1,13 @@ +const cache = require('memory-cache') +const providerCache = new cache.Cache() -module.exports = { +const photon = { commonName: 'Photon', DEFAULT_ENDPOINT: 'https://photon.komoot.io/api/', endpoint: (req, res) => { - return res.locals.settings.geocoding_provider || this.DEFAULT_ENDPOINT + return res.locals.settings.geocoding_provider || photon.DEFAULT_ENDPOINT }, + cache: providerCache, getParams (req, res) { const details = req.params.place_details @@ -15,4 +18,6 @@ module.exports = { } } -} \ No newline at end of file +} + +module.exports = photon \ No newline at end of file