From 5501d5a4f892ac1f9fee07ea248cd8ef084d4810 Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Thu, 28 Jan 2021 17:18:27 +0100 Subject: [PATCH 01/14] Configuration: add parameters limits --- api/v01/api_base.rb | 28 ++++++++++++++++++++ api/v01/isoline.rb | 1 - api/v01/matrix.rb | 8 +++++- api/v01/route.rb | 13 +++++++--- config/access.rb | 3 ++- config/environments/development.rb | 3 +++ config/environments/production.rb | 3 +++ config/environments/test.rb | 4 +++ test/api/v01/api_base_test.rb | 41 ++++++++++++++++++++++++++++++ test/api/v01/matrix_test.rb | 18 +++++++++++++ test/api/v01/route_test.rb | 24 +++++++++++++++++ 11 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 test/api/v01/api_base_test.rb diff --git a/api/v01/api_base.rb b/api/v01/api_base.rb index f39a257..d2bf4c3 100644 --- a/api/v01/api_base.rb +++ b/api/v01/api_base.rb @@ -28,6 +28,34 @@ def self.profile(api_key) ::RouterWrapper.access[api_key].except(:profile) ) end + + def self.count_locations(obj) + return 0 if obj.nil? + + if obj.is_a? Array + # route, matrix and isoline can send array like : + # [[[lat, lng], [lat, lng]]] or [[lat, lng], [lat, lng]] or [lat, lng, lat, lng] + obj.flatten.size / 2 + else + obj.split(',').size / 2 # matrix, isoline, route : "lat,lng,lat,lng" + end + end + + def self.count_route_locations(params) + if params[:loc] + count_locations(params[:loc]) + elsif params[:locs] + count_locations(params[:locs]) + end + end + + def self.count_matrix_locations(params) + if params[:dst] && params[:src] + count_locations(params[:src]) * count_locations(params[:dst]) + elsif params[:src] && !params[:dst] + count_locations(params[:src]) * count_locations(params[:src]) + end + end end end end diff --git a/api/v01/isoline.rb b/api/v01/isoline.rb index 2386bfa..cf64222 100644 --- a/api/v01/isoline.rb +++ b/api/v01/isoline.rb @@ -41,7 +41,6 @@ class Isoline < APIBase optional :departure, type: DateTime, desc: 'Departure date time (only used if router supports traffic).' optional :speed_multiplier, type: Float, desc: 'Speed multiplier (default: 1), not available on all transport modes.' optional :speed_multiplicator, type: Float, desc: 'Deprecated, use speed_multiplier instead.' -# optional :area, type: Array[Array[Float]], coerce_with: ->(c) { c.split(';').collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with semicolons (only available for truck mode at this time).' optional :area, type: Array, coerce_with: ->(c) { c.split(/;|\|/).collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplier_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Speed multiplier per area, 0 avoid area. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplicator_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Deprecated, use speed_multiplier_area instead.', documentation: { param_type: 'query' } diff --git a/api/v01/matrix.rb b/api/v01/matrix.rb index 56b1ef0..2d12684 100644 --- a/api/v01/matrix.rb +++ b/api/v01/matrix.rb @@ -35,6 +35,13 @@ class Matrix < APIBase # formatter :gpx, GpxFormatter default_format :json + before do + params_limit = APIBase.services(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) + if !params_limit[:locations].nil? + error!({message: "Exceeded \"matrix\" limit authorized for your account: #{params_limit[:locations]}. Please contact support or sales to increase limits."}, 413) if APIBase.count_matrix_locations(params) > params_limit[:locations] + end + end + params { optional :mode, type: Symbol, desc: 'Transportation mode (see capability operation for available modes).' optional :dimension, type: Symbol, values: [:time, :time_distance, :distance, :distance_time], default: :time, desc: 'Compute fastest or shortest and the optional additional dimension (default on time.)' @@ -42,7 +49,6 @@ class Matrix < APIBase optional :departure, type: DateTime, desc: 'Departure date time (only used if router supports traffic).' optional :speed_multiplier, type: Float, desc: 'Speed multiplier (default: 1), not available on all transport modes.' optional :speed_multiplicator, type: Float, desc: 'Deprecated, use speed_multiplier instead.' -# optional :area, type: Array[Array[Float]], coerce_with: ->(c) { c.split(';').collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with semicolons (only available for truck mode at this time).' optional :area, type: Array, coerce_with: ->(c) { c.split(/;|\|/).collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplier_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Speed multiplier per area, 0 avoid area. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplicator_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Deprecated, use speed_multiplier_area instead.', documentation: { param_type: 'query' } diff --git a/api/v01/route.rb b/api/v01/route.rb index dce9340..85fdc7a 100644 --- a/api/v01/route.rb +++ b/api/v01/route.rb @@ -36,6 +36,13 @@ class Route < APIBase # formatter :gpx, GpxFormatter default_format :json + before do + params_limit = APIBase.services(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) + if !params_limit[:locations].nil? + error!({message: "Exceeded \"routes\" limit authorized for your account: #{params_limit[:locations]}. Please contact support or sales to increase limits."}, 413) if APIBase.count_route_locations(params) > params_limit[:locations] + end + end + resource :route do desc 'Route via two points or more', { detail: 'Find the route between two or more points depending of transportation mode, dimension, etc... Area/speed_multiplier_area can be used to define areas where not to go or with heavy traffic (only available for truck mode at this time, see capability operation for information).', @@ -57,7 +64,7 @@ class Route < APIBase optional :arrival, type: DateTime, desc: 'Arrival date time (not used by all transport modes). In exclusion with departure.' optional :speed_multiplier, type: Float, desc: 'Speed multiplier (default: 1), not available on all transport modes.' optional :speed_multiplicator, type: Float, desc: 'Deprecated, use speed_multiplier instead.' -# optional :area, type: Array[Array[Float]], coerce_with: ->(c) { c.split(';').collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with semicolons (only available for truck mode at this time).' + optional :area, type: Array, coerce_with: ->(c) { c.split(/;|\|/).collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with pipes (only available for truck mode at this time).' optional :speed_multiplier_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Speed multiplier per area, 0 avoid area. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplicator_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Deprecated, use speed_multiplier_area instead.', documentation: { param_type: 'query' } @@ -78,7 +85,7 @@ class Route < APIBase optional :snap, type: Float, desc: 'Snap waypoint to junction close by snap distance.' optional :strict_restriction, type: Boolean, default: true, desc: 'Strict compliance with truck limitations.' optional :lang, type: String, default: :en -# requires :loc, type: Array[Array[Float]], coerce_with: ->(c) { c.split(',').collect{ |f| Float(f) }.each_slice(2).to_a }, desc: 'List of latitudes and longitudes separated with commas, e.g. lat1,lng1,lat2,lng2...' + requires :loc, type: Array[Float], coerce_with: ->(c) { c.split(',').collect{ |f| Float(f) } }, desc: 'List of latitudes and longitudes separated with commas, e.g. lat1,lng1,lat2,lng2...', documentation: { param_type: 'query' } optional :precision, type: Integer, default: 6, desc: 'Precison for encoded polyline.' optional :with_summed_by_area, type: Boolean, default: false, desc: 'Returns way type detail when set to true.' @@ -100,7 +107,6 @@ class Route < APIBase optional :arrival, type: DateTime, desc: 'Arrival date time (not used by all transport modes). In exclusion with departure.' optional :speed_multiplier, type: Float, desc: 'Speed multiplier (default: 1), not available on all transport modes.' optional :speed_multiplicator, type: Float, desc: 'Deprecated, use speed_multiplier instead.' -# optional :area, type: Array[Array[Float]], coerce_with: ->(c) { c.split(';').collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with semicolons (only available for truck mode at this time, see capability operation for informations).' optional :area, type: Array, coerce_with: ->(c) { c.split(/;|\|/).collect{ |b| b.split(',').collect{ |f| Float(f) }}}, desc: 'List of latitudes and longitudes separated with commas. Areas separated with pipes (only available for truck mode at this time, see capability operation for informations).', documentation: { param_type: 'query' } optional :speed_multiplier_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Speed multiplier per area, 0 avoid area. Areas separated with pipes (only available for truck mode at this time).', documentation: { param_type: 'query' } optional :speed_multiplicator_area, type: Array[Float], coerce_with: ->(c) { c.split(/;|\|/).collect{ |f| Float(f) }}, desc: 'Deprecated, use speed_multiplier_area instead.', documentation: { param_type: 'query' } @@ -121,7 +127,6 @@ class Route < APIBase optional :snap, type: Float, desc: 'Snap waypoint to junction close by snap distance.' optional :strict_restriction, type: Boolean, default: true, desc: 'Strict compliance with truck limitations.' optional :lang, type: String, default: :en -# requires :locs, type: Array[Array[Array[Float]]], coerce_with: ->(c) { c.split(';').collect{ |b| b.split(',').collect{ |f| Float(f) }.each_slice(2).to_a } }, desc: 'List of latitudes and longitudes separated with commas. Each route separated with semicolons. E.g. r1lat1,r1lng1,r1lat2,r1lng2;r2lat1,r2lng1,r2lat2,r2lng2' requires :locs, type: Array[String], coerce_with: ->(c) { c.split(/;|\|/) }, desc: 'List of latitudes and longitudes separated with commas. Each route separated by pipes. E.g. r1lat1,r1lng1,r1lat2,r1lng2|r2lat1,r2lng1,r2lat2,r2lng2', documentation: { param_type: 'query' } optional :precision, type: Integer, default: 6, desc: 'Precison for encoded polyline.' } diff --git a/config/access.rb b/config/access.rb index a30a4d2..c8c3efe 100644 --- a/config/access.rb +++ b/config/access.rb @@ -18,6 +18,7 @@ module RouterWrapper @access_by_api_key = { # params_limit overload values from profile - 'demo' => { profile: :standard } + 'demo' => { profile: :standard }, + 'demo_limit' => { profile: :standard, params_limit: { locations: 1 } }, } end diff --git a/config/environments/development.rb b/config/environments/development.rb index 2310a30..639d251 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -64,6 +64,8 @@ module RouterWrapper HERE_TRUCK = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'truck') HERE_CAR = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'car') + PARAMS_LIMIT = { locations: 1000 }.freeze + @@c = { product_title: 'Router Wrapper API', product_contact_email: 'tech@mapotempo.com', @@ -74,6 +76,7 @@ module RouterWrapper profiles: { light: { route_default: :crow, + params_limit: PARAMS_LIMIT, route: { crow: [CROW], }, diff --git a/config/environments/production.rb b/config/environments/production.rb index a6a7965..f231c63 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -39,6 +39,8 @@ module RouterWrapper HERE_APP_CODE = nil HERE_TRUCK = Wrappers::Here.new(CACHE, app_id: HERE_APP_ID, app_code: HERE_APP_CODE, mode: 'truck') + PARAMS_LIMIT = { locations: 1000 }.freeze + @@c = { product_title: 'Router Wrapper API', product_contact_email: 'tech@mapotempo.com', @@ -49,6 +51,7 @@ module RouterWrapper profiles: { light: { route_default: :crow, + params_limit: PARAMS_LIMIT, route: { crow: [CROW], }, diff --git a/config/environments/test.rb b/config/environments/test.rb index 6ce50a2..d2af58f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -63,6 +63,8 @@ module RouterWrapper HERE_TRUCK = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'truck') HERE_CAR = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'car') + PARAMS_LIMIT = { locations: 10000 }.freeze + @@c = { product_title: 'Router Wrapper API', product_contact_email: 'tech@mapotempo.com', @@ -73,6 +75,7 @@ module RouterWrapper profiles: { light: { route_default: :crow, + params_limit: PARAMS_LIMIT, route: { crow: [CROW], }, @@ -85,6 +88,7 @@ module RouterWrapper }, standard: { route_default: :crow, + params_limit: PARAMS_LIMIT, route: { crow: [CROW], osrm: [OSRM], diff --git a/test/api/v01/api_base_test.rb b/test/api/v01/api_base_test.rb new file mode 100644 index 0000000..4bf1f89 --- /dev/null +++ b/test/api/v01/api_base_test.rb @@ -0,0 +1,41 @@ +# Copyright © Mapotempo, 2021 +# +# This file is part of Mapotempo. +# +# Mapotempo is free software. You can redistribute it and/or +# modify since you respect the terms of the GNU Affero General +# Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# Mapotempo is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Mapotempo. If not, see: +# +# +require './test/test_helper' + +require './api/root' + +class Api::V01::APIBaseTest < Minitest::Test + def test_count_locations + lat, lng = 1, 1 + assert 2, Api::V01::APIBase.count_locations([[[lat, lng], [lat, lng]]]) + assert 2, Api::V01::APIBase.count_locations([[lat, lng], [lat, lng]]) + assert 2, Api::V01::APIBase.count_locations([lat, lng, lat, lng]) + assert 2, Api::V01::APIBase.count_locations('lat,lng,lat,lng') + assert 0, Api::V01::APIBase.count_locations(nil) + end + + def test_count_matrix_locations + assert 2, Api::V01::APIBase.count_matrix_locations(src: 'lat,lng,lat,lng', dest: 'lat,lng,lat,lng') + assert 2, Api::V01::APIBase.count_matrix_locations(src: 'lat,lng,lat,lng') + end + + def test_count_route_locations + assert 2, Api::V01::APIBase.count_matrix_locations(loc: 'lat,lng,lat,lng') + assert 2, Api::V01::APIBase.count_matrix_locations(locs: 'lat,lng,lat,lng') + end +end diff --git a/test/api/v01/matrix_test.rb b/test/api/v01/matrix_test.rb index 2cbeea9..465db43 100644 --- a/test/api/v01/matrix_test.rb +++ b/test/api/v01/matrix_test.rb @@ -180,4 +180,22 @@ def test_here_matrix_should_be_fifteen_per_hundred_at_max end end end + + def test_param_matrix_dont_exceed_limit + [:get, :post].each do |method| + send method, '/0.1/matrix', api_key: 'demo', src: '43.2804,5.3806,43.291576,5.355835', dst: '43.2804,5.3806,43.291577,5.355836' + assert 200, last_response.status + send method, '/0.1/matrix', api_key: 'demo', src: '43.2804,5.3806,43.291576,5.355835' + assert_includes last_response.body, 'matrix_time' + end + end + + def test_param_matrix_exceed_limit + [:get, :post].each do |method| + send method, '/0.1/matrix', api_key: 'demo_limit', src: '43.2804,5.3806,43.291576,5.355835', dst: '43.2804,5.3806,43.291577,5.355836' + assert 413, last_response.status + send method, '/0.1/matrix', api_key: 'demo_limit', src: '43.2804,5.3806,43.291576,5.355835' + assert_includes last_response.body, 'Exceeded' + end + end end diff --git a/test/api/v01/route_test.rb b/test/api/v01/route_test.rb index fcf5cb2..2c63f1c 100644 --- a/test/api/v01/route_test.rb +++ b/test/api/v01/route_test.rb @@ -123,4 +123,28 @@ def test_routes_odd_locs assert !last_response.ok?, last_response.body } end + + def test_param_route_dont_exceed_limit + [ + { method: :get, url: '/0.1/route', options: { api_key: 'demo', loc: '43.2804,5.3806,43.2804,5.3806' }}, + { method: :get, url: '/0.1/routes', options: { api_key: 'demo', locs: '43.2804,5.3806,43.2804,5.3806' }}, + { method: :post, url: '/0.1/routes', options: { api_key: 'demo', locs: '43.2804,5.3806,43.2804,5.3806' }} + ].each do |obj| + send obj[:method], obj[:url], obj[:options] + assert 200, last_response.status + assert_includes last_response.body, 'total_distance' + end + end + + def test_param_route_exceed_limit + [ + { method: :get, url: '/0.1/route', options: { api_key: 'demo_limit', loc: '43.2804,5.3806,43.2804,5.3806' }}, + { method: :get, url: '/0.1/routes', options: { api_key: 'demo_limit', locs: '43.2804,5.3806,43.2804,5.3806' }}, + { method: :post, url: '/0.1/routes', options: { api_key: 'demo_limit', locs: '43.2804,5.3806,43.2804,5.3806' }} + ].each do |obj| + send obj[:method], obj[:url], obj[:options] + assert 413, last_response.status + assert_includes last_response.body, 'Exceeded' + end + end end From c459afdc62211beac16a42692892be40619dfc1e Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Wed, 3 Feb 2021 18:22:41 +0100 Subject: [PATCH 02/14] Count api calls --- .gitignore | 3 + Gemfile | 3 +- Gemfile.lock | 3 + api/v01/api.rb | 92 ++++++++++++++++++++++++++++++ api/v01/api_base.rb | 2 + api/v01/isoline.rb | 7 ++- api/v01/matrix.rb | 13 +++-- api/v01/route.rb | 14 +++-- config/access.rb | 8 ++- config/environments/development.rb | 8 ++- config/environments/production.rb | 7 ++- config/environments/test.rb | 8 ++- docker/access.rb | 1 + docker/data-count/.empty | 0 docker/docker-compose.yml | 12 +++- test/api/v01/isoline_test.rb | 24 ++++++++ test/api/v01/matrix_test.rb | 28 +++++++++ test/api/v01/route_test.rb | 33 +++++++++++ test/test_helper.rb | 9 +++ 19 files changed, 256 insertions(+), 19 deletions(-) create mode 120000 docker/access.rb create mode 100644 docker/data-count/.empty diff --git a/.gitignore b/.gitignore index 187afe9..e7e5fff 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ build/ # Ignore application configuration local.env + +# Redis +*.aof diff --git a/Gemfile b/Gemfile index 50a34c4..5c8a7fc 100644 --- a/Gemfile +++ b/Gemfile @@ -21,10 +21,11 @@ gem 'border_patrol' gem 'activesupport' group :test do - gem 'rack-test' + gem 'fakeredis' gem 'minitest' gem 'minitest-focus' gem 'minitest-reporters' + gem 'rack-test' gem 'simplecov', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 662076a..c75d7e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,6 +27,8 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.2.1) equalizer (0.0.11) + fakeredis (0.8.0) + redis (~> 4.1) grape (1.2.3) activesupport builder @@ -125,6 +127,7 @@ DEPENDENCIES border_patrol byebug dotenv + fakeredis grape grape-entity grape-swagger diff --git a/api/v01/api.rb b/api/v01/api.rb index c1e8a73..9af4447 100644 --- a/api/v01/api.rb +++ b/api/v01/api.rb @@ -23,6 +23,17 @@ require './api/v01/isoline' require './api/v01/capability' +require 'date' + +class QuotaExceeded < StandardError + attr_reader :data + + def initialize(msg, data) + @data = data + super(msg) + end +end + module Api module V01 class Api < Grape::API @@ -30,6 +41,75 @@ class Api < Grape::API error!('401 Unauthorized', 401) unless params && RouterWrapper.access(true).keys.include?(params[:api_key]) end + helpers do + def redis_count + RouterWrapper.config[:redis_count] + end + + def count_time + @count_time ||= Time.now.utc + end + + def count_base_key(operation, period = :daily) + count_date = if period == :daily + count_time.to_s[0..9] + elsif period == :monthly + count_time.to_s[0..6] + elsif period == :yearly + count_time.to_s[0..3] + end + [ + [:router, operation, count_date].compact, + [:key, params[:api_key]] + ].map{ |a| a.join(':') }.join('_') + end + + def count_key(operation) + @count_key ||= count_base_key(operation) + '_' + [ + [:ip, (env['action_dispatch.remote_ip'] || request.ip).to_s], + [:asset, params[:asset]] + ].map{ |a| a.join(':') }.join('_') + end + + def count(operation, raise_if_exceed = true, request_size = 1) + return unless redis_count + + @count_val = redis_count.hgetall(count_key(operation)).symbolize_keys + if @count_val.empty? + @count_val = {hits: 0, transactions: 0} + redis_count.mapped_hmset @count_key, @count_val + redis_count.expire @count_key, 100.days + end + APIBase.profile(params[:api_key])[:quotas]&.each do |quota| + op = quota[:operation] + next unless op.nil? || op == operation + + quota.slice(:daily, :monthly, :yearly).each do |k, v| + count = redis_count.get(count_base_key(op, k)).to_i + raise QuotaExceeded.new("Too many #{k} requests", limit: v, remaining: v - count, reset: k) if count + request_size > v + end + end if raise_if_exceed + end + + def count_incr(operation, options) + return unless redis_count + + count operation, false unless @count_val + incr = {hits: @count_val[:hits].to_i + 1} + incr[:transactions] = @count_val[:transactions].to_i + options[:transactions] if options[:transactions] + redis_count.mapped_hmset @count_key, incr + APIBase.profile(params[:api_key])[:quotas]&.each do |quota| + op = quota[:operation] + next unless op.nil? || op == operation + + quota.slice(:daily, :monthly, :yearly).each do |k, v| + redis_count.incrby count_base_key(op, k), options[:transactions] + redis_count.expire count_base_key(op, k), 366.days + end + end if options[:transactions] + end + end + rescue_from :all, backtrace: ENV['APP_ENV'] != 'production' do |e| @error = e if ENV['APP_ENV'] != 'test' @@ -49,6 +129,18 @@ class Api < Grape::API rack_response(format_message(response, nil), 400) elsif e.is_a?(Wrappers::UnreachablePointError) rack_response(format_message(response, nil), 204) + elsif e.is_a?(QuotaExceeded) + headers = { 'Content-Type' => content_type, + 'X-RateLimit-Limit' => e.data[:limit], + 'X-RateLimit-Remaining' => e.data[:remaining], + 'X-RateLimit-Reset' => if e.data[:reset] == :daily + count_time.to_date.next_day + elsif e.data[:reset] == :monthly + count_time.to_date.next_month + elsif e.data[:reset] == :yearly + count_time.to_date.next_year + end.to_time.to_i } + rack_response(format_message(response, nil), 429, headers) else rack_response(format_message(response, e.backtrace), 500) end diff --git a/api/v01/api_base.rb b/api/v01/api_base.rb index d2bf4c3..4db21d2 100644 --- a/api/v01/api_base.rb +++ b/api/v01/api_base.rb @@ -29,6 +29,8 @@ def self.profile(api_key) ) end + ## + # @param obj can be a string or an array def self.count_locations(obj) return 0 if obj.nil? diff --git a/api/v01/isoline.rb b/api/v01/isoline.rb index cf64222..f63971c 100644 --- a/api/v01/isoline.rb +++ b/api/v01/isoline.rb @@ -78,6 +78,7 @@ class Isoline < APIBase get do params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :isoline, true, APIBase.count_locations(params['loc']) isoline params end @@ -95,6 +96,7 @@ class Isoline < APIBase post do params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :isoline, true, APIBase.count_locations(params['loc']) isoline params status 200 end @@ -104,13 +106,14 @@ class Isoline < APIBase def isoline(params) params[:mode] ||= APIBase.profile(params[:api_key])[:route_default] if params[:area] - params[:area].all?{ |area| area.size % 2 == 0 } || error!({detail: 'area: couples of lat/lng are needed.'}, 400) + params[:area].all?{ |area| area.size % 2 == 0 } || error!('area: couples of lat/lng are needed.', 400) params[:area] = params[:area].collect{ |area| area.each_slice(2).to_a } end - params[:loc].size == 2 || error!({detail: 'Start lat/lng is needed.'}, 400) + params[:loc].size == 2 || error!('Start lat/lng is needed.', 400) results = RouterWrapper::wrapper_isoline(APIBase.profile(params[:api_key]), params) results[:router][:version] = 'draft' + count_incr :isoline, transactions: APIBase.count_locations(params['loc']) present results, with: IsolineResult, geometry: true end end diff --git a/api/v01/matrix.rb b/api/v01/matrix.rb index 2d12684..f84c86f 100644 --- a/api/v01/matrix.rb +++ b/api/v01/matrix.rb @@ -36,7 +36,7 @@ class Matrix < APIBase default_format :json before do - params_limit = APIBase.services(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) + params_limit = APIBase.profile(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) if !params_limit[:locations].nil? error!({message: "Exceeded \"matrix\" limit authorized for your account: #{params_limit[:locations]}. Please contact support or sales to increase limits."}, 413) if APIBase.count_matrix_locations(params) > params_limit[:locations] end @@ -80,6 +80,7 @@ class Matrix < APIBase get do params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :matrix, true, APIBase.count_matrix_locations(params) matrix params end @@ -92,6 +93,7 @@ class Matrix < APIBase post do params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :matrix, true, APIBase.count_matrix_locations(params) matrix params status 200 end @@ -101,21 +103,22 @@ class Matrix < APIBase def matrix(params) params[:mode] ||= APIBase.profile(params[:api_key])[:route_default] if params[:area] - params[:area].all?{ |area| (area.size % 2).zero? } || error!({detail: 'area: couples of lat/lng are needed.'}, 400) + params[:area].all?{ |area| (area.size % 2).zero? } || error!('area: couples of lat/lng are needed.', 400) params[:area] = params[:area].collect{ |area| area.each_slice(2).to_a } end params[:src] = params[:src].each_slice(2).to_a - params[:src][-1].size == 2 || error!({detail: 'Source couples of lat/lng are needed.'}, 400) + params[:src][-1].size == 2 || error!('Source couples of lat/lng are needed.', 400) if params.key?(:dst) && !params[:dst].empty? params[:dst] = params[:dst].each_slice(2).to_a - params[:dst][-1].size == 2 || error!({detail: 'Destination couples of lat/lng are needed.'}, 400) + params[:dst][-1].size == 2 || error!('Destination couples of lat/lng are needed.', 400) else params[:dst] = params[:src] end - results = RouterWrapper::wrapper_matrix(APIBase.profile(params[:api_key]), params) + results = RouterWrapper.wrapper_matrix(APIBase.profile(params[:api_key]), params) results[:router][:version] = 'draft' + count_incr :matrix, transactions: APIBase.count_matrix_locations(params) present results, with: MatrixResult end end diff --git a/api/v01/route.rb b/api/v01/route.rb index 85fdc7a..abd47f9 100644 --- a/api/v01/route.rb +++ b/api/v01/route.rb @@ -37,7 +37,7 @@ class Route < APIBase default_format :json before do - params_limit = APIBase.services(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) + params_limit = APIBase.profile(params[:api_key])[:params_limit].merge(RouterWrapper.access[params[:api_key]][:params_limit] || {}) if !params_limit[:locations].nil? error!({message: "Exceeded \"routes\" limit authorized for your account: #{params_limit[:locations]}. Please contact support or sales to increase limits."}, 413) if APIBase.count_route_locations(params) > params_limit[:locations] end @@ -85,7 +85,6 @@ class Route < APIBase optional :snap, type: Float, desc: 'Snap waypoint to junction close by snap distance.' optional :strict_restriction, type: Boolean, default: true, desc: 'Strict compliance with truck limitations.' optional :lang, type: String, default: :en - requires :loc, type: Array[Float], coerce_with: ->(c) { c.split(',').collect{ |f| Float(f) } }, desc: 'List of latitudes and longitudes separated with commas, e.g. lat1,lng1,lat2,lng2...', documentation: { param_type: 'query' } optional :precision, type: Integer, default: 6, desc: 'Precison for encoded polyline.' optional :with_summed_by_area, type: Boolean, default: false, desc: 'Returns way type detail when set to true.' @@ -94,6 +93,7 @@ class Route < APIBase params[:locs] = [params[:loc].each_slice(2).to_a] params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if !params[:speed_multiplier_area] + count :route, true, APIBase.count_route_locations(params) present compute_routes(params)[0], with: RouteResult, geometry: params[:geometry], toll_costs: params[:toll_costs], with_summed_by_area: params[:with_summed_by_area] end end @@ -146,6 +146,7 @@ class Route < APIBase params[:locs] = params[:locs].collect{ |b| b.split(',').collect{ |f| Float(f) }.each_slice(2).to_a } params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :route, true, APIBase.count_route_locations(params) many_routes params end @@ -164,6 +165,7 @@ class Route < APIBase params[:locs] = params[:locs].collect{ |b| b.split(',').collect{ |f| Float(f) }.each_slice(2).to_a } params[:speed_multiplier] = params[:speed_multiplicator] if !params[:speed_multiplier] params[:speed_multiplier_area] = params[:speed_multiplicator_area] if params[:speed_multiplicator_area] && params[:speed_multiplicator_area].size > 0 && (!params[:speed_multiplier_area] || params[:speed_multiplier_area].size == 0) + count :route, true, APIBase.count_route_locations(params) many_routes params status 200 end @@ -173,12 +175,12 @@ class Route < APIBase def compute_routes(params) params[:mode] ||= APIBase.profile(params[:api_key])[:route_default] if params[:area] - params[:area].all?{ |area| area.size % 2 == 0 } || error!({detail: 'area: couples of lat/lng required.'}, 400) + params[:area].all?{ |area| area.size % 2 == 0 } || error!('area: couples of lat/lng required.', 400) params[:area] = params[:area].collect{ |area| area.each_slice(2).to_a } end params[:locs].each_with_index{ |loc, index| - loc.size >= 2 || error!({detail: "locs: segment ##{index}, at least two couples of lat/lng required."}, 400) - loc[-1].size == 2 || error!({detail: "locs: segment ##{index}, couples of lat/lng required."}, 400) + loc.size >= 2 || error!("locs: segment ##{index}, at least two couples of lat/lng required.", 400) + loc[-1].size == 2 || error!("locs: segment ##{index}, couples of lat/lng required.", 400) } errors_count = 0 @@ -216,6 +218,8 @@ def compute_routes(params) end end } + count_incr :route, transactions: APIBase.count_route_locations(params) + routes end def many_routes(params) diff --git a/config/access.rb b/config/access.rb index c8c3efe..00588d5 100644 --- a/config/access.rb +++ b/config/access.rb @@ -19,6 +19,12 @@ module RouterWrapper @access_by_api_key = { # params_limit overload values from profile 'demo' => { profile: :standard }, - 'demo_limit' => { profile: :standard, params_limit: { locations: 1 } }, + 'demo_limit' => { profile: :standard, params_limit: { locations: 1 }}, + 'demo_quotas' => { profile: :standard, params_limit: { locations: 2 }, quotas: [ + { operation: :route, daily: 2 }, + { operation: :isoline, daily: 1 }, + { operation: :matrix, daily: 2 }, + { monthly: 4 } + ]}, } end diff --git a/config/environments/development.rb b/config/environments/development.rb index 639d251..d4bda54 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,6 +16,8 @@ # # require 'active_support' +require 'active_support/core_ext' +require 'byebug' require 'dotenv' require 'tmpdir' require 'byebug' @@ -65,6 +67,8 @@ module RouterWrapper HERE_CAR = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'car') PARAMS_LIMIT = { locations: 1000 }.freeze + REDIS_COUNT = ENV['REDIS_COUNT_HOST'] && Redis.new(host: ENV['REDIS_COUNT_HOST']) + QUOTAS = [{ daily: 100000, monthly: 1000000, yearly: 10000000 }].freeze # Only taken into account if REDIS_COUNT @@c = { product_title: 'Router Wrapper API', @@ -77,6 +81,7 @@ module RouterWrapper light: { route_default: :crow, params_limit: PARAMS_LIMIT, + quotas: QUOTAS, # Only taken into account if REDIS_COUNT route: { crow: [CROW], }, @@ -109,6 +114,7 @@ module RouterWrapper otp: [OTP_BORDEAUX], } } - } + }, + redis_count: REDIS_COUNT, } end diff --git a/config/environments/production.rb b/config/environments/production.rb index f231c63..607cd5d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -16,6 +16,7 @@ # # require 'active_support' +require 'active_support/core_ext' require 'dotenv' require 'tmpdir' @@ -40,6 +41,8 @@ module RouterWrapper HERE_TRUCK = Wrappers::Here.new(CACHE, app_id: HERE_APP_ID, app_code: HERE_APP_CODE, mode: 'truck') PARAMS_LIMIT = { locations: 1000 }.freeze + REDIS_COUNT = ENV['REDIS_COUNT_HOST'] && Redis.new(host: ENV['REDIS_COUNT_HOST']) + QUOTAS = [{ daily: 100000, monthly: 1000000 }].freeze # Only taken into account if REDIS_COUNT @@c = { product_title: 'Router Wrapper API', @@ -52,6 +55,7 @@ module RouterWrapper light: { route_default: :crow, params_limit: PARAMS_LIMIT, + quotas: QUOTAS, # Only taken into account if REDIS_COUNT route: { crow: [CROW], }, @@ -81,6 +85,7 @@ module RouterWrapper otp: [OTP_BORDEAUX], } } - } + }, + redis_count: REDIS_COUNT, } end diff --git a/config/environments/test.rb b/config/environments/test.rb index d2af58f..d202bfd 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -16,6 +16,7 @@ # # require 'active_support' +require 'active_support/core_ext' require 'byebug' require 'dotenv' require 'tmpdir' @@ -64,6 +65,8 @@ module RouterWrapper HERE_CAR = Wrappers::Here.new(CACHE, app_id: ENV['HERE_APP_ID'], app_code: ENV['HERE_APP_CODE'], mode: 'car') PARAMS_LIMIT = { locations: 10000 }.freeze + REDIS_COUNT = Redis.new # Fake redis + QUOTAS = [{ daily: 100000, monthly: 1000000, yearly: 10000000 }].freeze # Only taken into account if REDIS_COUNT @@c = { product_title: 'Router Wrapper API', @@ -76,6 +79,7 @@ module RouterWrapper light: { route_default: :crow, params_limit: PARAMS_LIMIT, + quotas: QUOTAS, # Only taken into account if REDIS_COUNT route: { crow: [CROW], }, @@ -89,6 +93,7 @@ module RouterWrapper standard: { route_default: :crow, params_limit: PARAMS_LIMIT, + quotas: QUOTAS, # Only taken into account if REDIS_COUNT route: { crow: [CROW], osrm: [OSRM], @@ -108,6 +113,7 @@ module RouterWrapper here: [HERE_TRUCK], } } - } + }, + redis_count: REDIS_COUNT, } end diff --git a/docker/access.rb b/docker/access.rb new file mode 120000 index 0000000..e81a6ee --- /dev/null +++ b/docker/access.rb @@ -0,0 +1 @@ +../config/access.rb \ No newline at end of file diff --git a/docker/data-count/.empty b/docker/data-count/.empty new file mode 100644 index 0000000..e69de29 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index aeab8f1..49fed91 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,14 +8,16 @@ services: ports: - "8082:80" # HOST:CONTAINER, edit only HOST part volumes: - - ./production.rb:/srv/app/config/environments/production.rb - - ./production.rb:/srv/app/config/environments/development.rb + - ./access.rb:/srv/app/config/access.rb - ./poly:/srv/app/poly restart: always links: - redis-cache + - redis-count environment: PASSENGER_APP_ENV: production + REDIS_HOST: redis-cache + REDIS_COUNT_HOST: redis-count otp-bordeaux: build: @@ -44,3 +46,9 @@ services: image: redis:${REDIS_VERSION:-3.2-alpine} command: redis-server --save "" restart: always + + redis-count: + image: redis:${REDIS_VERSION:-3.2-alpine} + volumes: + - ./data-count:/data + command: redis-server --appendonly yes diff --git a/test/api/v01/isoline_test.rb b/test/api/v01/isoline_test.rb index dfdd574..8964eb2 100644 --- a/test/api/v01/isoline_test.rb +++ b/test/api/v01/isoline_test.rb @@ -57,4 +57,28 @@ def test_isoline_invalid_query_string_malformed get '/0.1/isoline', api_key: 'demo', loc: '48.726675,-0.000079', size: 1, mode: 'osrm' assert last_response.ok? end + + def test_count_isolines + [:get, :post].each_with_index do |method, indx| + send method, '/0.1/isoline', {api_key: 'demo', loc: '43.2804,5.3806', size: 1} + keys = RouterWrapper.config[:redis_count].keys("router:isoline:#{Time.now.utc.to_s[0..9]}_key:demo_ip*") + assert_equal 1, keys.size + assert_equal({'hits' => (indx + 1).to_s, 'transactions' => (indx + 1).to_s}, RouterWrapper.config[:redis_count].hgetall(keys.first)) + end + end + + def test_use_quotas + loc = '43.2804,5.3806' + + post '/0.1/isoline', {api_key: 'demo_quotas', loc: loc, size: 1} + assert last_response.ok?, last_response.body + + post '/0.1/isoline', {api_key: 'demo_quotas', loc: loc, size: 1} + assert_equal 429, last_response.status + + assert_includes JSON.parse(last_response.body)['message'], 'Too many daily requests' + assert_equal ['application/json; charset=UTF-8', 1, 0, Time.now.utc.to_date.next_day.to_time.to_i], last_response.headers.select{ |key| + key =~ /(Content-Type|X-RateLimit-Limit|X-RateLimit-Remaining|X-RateLimit-Reset)/ + }.values + end end diff --git a/test/api/v01/matrix_test.rb b/test/api/v01/matrix_test.rb index 465db43..41c6cc1 100644 --- a/test/api/v01/matrix_test.rb +++ b/test/api/v01/matrix_test.rb @@ -198,4 +198,32 @@ def test_param_matrix_exceed_limit assert_includes last_response.body, 'Exceeded' end end + + def test_count_matrix + [:get, :post].each_with_index do |method, indx| + src = '43.2804,5.3806,43.2804,5.3806' + dst = '43.2804,5.3806' + send method, '/0.1/matrix', {api_key: 'demo', src: src, dst: dst} + keys = RouterWrapper.config[:redis_count].keys("router:matrix:#{Time.now.utc.to_s[0..9]}_key:demo_ip*") + assert_equal 1, keys.size + transactions = Api::V01::APIBase.count_matrix_locations(src: src, dst: dst) + assert_equal({'hits' => (indx + 1).to_s, 'transactions' => ((indx + 1) * transactions).to_s}, RouterWrapper.config[:redis_count].hgetall(keys.first)) + end + end + + def test_use_quotas + src = '43.2804,5.3806,43.2804,5.3806' + dst = '43.2804,5.3806' + + post '/0.1/matrix', {api_key: 'demo_quotas', src: src, dst: dst} + assert last_response.ok?, last_response.body + + post '/0.1/matrix', {api_key: 'demo_quotas', src: src, dst: dst} + assert_equal 429, last_response.status + + assert_includes JSON.parse(last_response.body)['message'], 'Too many daily requests' + assert_equal ['application/json; charset=UTF-8', 2, 0, Time.now.utc.to_date.next_day.to_time.to_i], last_response.headers.select{ |key| + key =~ /(Content-Type|X-RateLimit-Limit|X-RateLimit-Remaining|X-RateLimit-Reset)/ + }.values + end end diff --git a/test/api/v01/route_test.rb b/test/api/v01/route_test.rb index 2c63f1c..ccb0d75 100644 --- a/test/api/v01/route_test.rb +++ b/test/api/v01/route_test.rb @@ -147,4 +147,37 @@ def test_param_route_exceed_limit assert_includes last_response.body, 'Exceeded' end end + + def test_count_routes + locs = '43.2804,5.3806,43.2804,5.3806' + [ + { method: 'get', url: '/0.1/route', + options: {api_key: 'demo', loc: locs} }, + { method: 'get', url: '/0.1/routes', + options: {api_key: 'demo', locs: locs} }, + { method: 'post', url: '/0.1/routes', + options: {api_key: 'demo', locs: locs} }, + ].each_with_index do |obj, indx| + send obj[:method], obj[:url], obj[:options] + keys = RouterWrapper.config[:redis_count].keys("router:route:#{Time.now.utc.to_s[0..9]}_key:demo_ip*") + assert_equal 1, keys.size + transactions = Api::V01::APIBase.count_route_locations(locs: locs) + assert_equal({'hits' => (indx + 1).to_s, 'transactions' => ((indx + 1) * transactions).to_s}, RouterWrapper.config[:redis_count].hgetall(keys.first)) + end + end + + def test_use_quotas + locs = '43.2804,5.3806,43.2804,5.3806' + + post '/0.1/routes', {api_key: 'demo_quotas', locs: locs} + assert last_response.ok?, last_response.body + + post '/0.1/routes', {api_key: 'demo_quotas', locs: locs} + assert_equal 429, last_response.status + + assert_includes JSON.parse(last_response.body)['message'], 'Too many daily requests' + assert_equal ['application/json; charset=UTF-8', 2, 0, Time.now.utc.to_date.next_day.to_time.to_i], last_response.headers.select{ |key| + key =~ /(Content-Type|X-RateLimit-Limit|X-RateLimit-Remaining|X-RateLimit-Reset)/ + }.values + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index fde27ce..b1e27ad 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,6 +18,9 @@ require 'simplecov' SimpleCov.start +require 'minitest' +require 'fakeredis/minitest' + ENV['APP_ENV'] ||= 'test' require File.expand_path('../../config/environments/' + ENV['APP_ENV'], __FILE__) Dir[File.dirname(__FILE__) + '/../config/initializers/*.rb'].each {|file| require file } @@ -62,3 +65,9 @@ def random_location(centroid, max_radius) random_lng = centroid[:lng] + dx / (one_degree * Math.cos(centroid[:lat] * Math::PI / 180)) [random_lat.round(5), random_lng.round(5)] # meter precision end + +module FakeRedis + def teardown + RouterWrapper.config[:redis_count].flushall + end +end From 8a68780737314f94dd2d55d81b891e82adc3d5a0 Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Thu, 4 Feb 2021 09:26:21 +0100 Subject: [PATCH 03/14] Action pack for remote_ip --- Gemfile | 1 + Gemfile.lock | 158 +++++++++++++++++++++++++++++++-------------------- server.ru | 3 + 3 files changed, 99 insertions(+), 63 deletions(-) diff --git a/Gemfile b/Gemfile index 5c8a7fc..b447a36 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem 'rest-client' gem 'addressable' gem 'border_patrol' gem 'activesupport' +gem 'actionpack' group :test do gem 'fakeredis' diff --git a/Gemfile.lock b/Gemfile.lock index c75d7e2..dfd12b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,81 +1,109 @@ GEM remote: https://rubygems.org/ specs: - activesupport (5.2.4.3) + actionpack (5.2.4.5) + actionview (= 5.2.4.5) + activesupport (= 5.2.4.5) + rack (~> 2.0, >= 2.0.8) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.4.5) + activesupport (= 5.2.4.5) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activesupport (5.2.4.5) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.8.0) + addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) ansi (1.5.0) - axiom-types (0.1.1) - descendants_tracker (~> 0.0.4) - ice_nine (~> 0.11.0) - thread_safe (~> 0.3, >= 0.3.1) border_patrol (0.2.1) nokogiri (>= 1.4.3.1) - builder (3.2.3) - byebug (10.0.0) - coercible (1.0.0) - descendants_tracker (~> 0.0.1) + builder (3.2.4) + byebug (11.1.3) concurrent-ruby (1.1.8) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) - docile (1.1.5) - domain_name (0.5.20170404) + crass (1.0.6) + docile (1.3.5) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.2.1) - equalizer (0.0.11) + dotenv (2.7.6) + dry-configurable (0.12.1) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5.0) + dry-container (0.7.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.5.0) + concurrent-ruby (~> 1.0) + dry-inflector (0.2.0) + dry-logic (1.1.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5) + dry-types (1.5.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.5, >= 0.5) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) + erubi (1.10.0) fakeredis (0.8.0) redis (~> 4.1) - grape (1.2.3) + grape (1.5.2) activesupport builder + dry-types (>= 1.1) mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept - virtus (>= 1.0.0) - grape-entity (0.7.1) - activesupport (>= 4.0) + grape-entity (0.8.2) + activesupport (>= 3.0.0) multi_json (>= 1.3.2) - grape-swagger (0.32.1) - grape (>= 0.16.2) - grape-swagger-entity (0.2.3) - grape-entity (>= 0.5.0) - grape-swagger (>= 0.20.4) - grape_logging (1.7.0) + grape-swagger (1.3.1) + grape (~> 1.3) + grape-swagger-entity (0.5.1) + grape-entity (>= 0.6.0) + grape-swagger (>= 1.2.0) + grape_logging (1.8.4) grape + rack + http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (1.6.0) + i18n (1.8.9) concurrent-ruby (~> 1.0) - ice_nine (0.11.2) - json (2.5.1) - mime-types (3.1) + loofah (2.9.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mini_portile2 (2.5.3) - minitest (5.11.3) - minitest-focus (1.1.2) + mime-types-data (3.2021.0212) + mini_portile2 (2.5.0) + minitest (5.14.3) + minitest-focus (1.2.1) minitest (>= 4, < 6) - minitest-reporters (1.1.19) + minitest-reporters (1.4.3) ansi builder minitest (>= 5.0) ruby-progressbar - multi_json (1.13.1) - mustermann (1.0.3) - mustermann-grape (1.0.0) - mustermann (~> 1.0.0) + multi_json (1.15.0) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + mustermann-grape (1.0.1) + mustermann (>= 1.0.0) netrc (0.11.0) - nio4r (2.5.2) - nokogiri (1.11.7) + nio4r (2.5.5) + nokogiri (1.11.1) mini_portile2 (~> 2.5.0) racc (~> 1.4) - polylines (0.3.0) + polylines (0.4.0) public_suffix (4.0.6) - puma (4.3.8) + puma (5.2.1) nio4r (~> 2.0) racc (1.5.2) rack (2.2.3) @@ -83,45 +111,49 @@ GEM rack (>= 0.4) rack-contrib (2.3.0) rack (~> 2.0) - rack-cors (1.0.5) - rack (>= 1.6.0) + rack-cors (1.1.1) + rack (>= 2.0.0) rack-server-pages (0.1.0) rack - rack-test (0.8.2) + rack-test (1.1.0) rack (>= 1.0, < 3) - rake (12.3.3) - redis (4.1.2) - redis-activesupport (5.0.4) - activesupport (>= 3, < 6) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) + rake (13.0.3) + redis (4.2.5) + redis-activesupport (5.2.0) + activesupport (>= 3, < 7) redis-store (>= 1.3, < 2) redis-store (1.4.1) redis (>= 2.2, < 5) - rest-client (2.0.2) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - ruby-progressbar (1.9.0) - simplecov (0.15.1) - docile (~> 1.1.0) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.4) + simplecov (0.21.2) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.2) thread_safe (0.3.6) tzinfo (1.2.9) thread_safe (~> 0.1) unf (0.1.4) unf_ext - unf_ext (0.0.7.4) - virtus (1.0.5) - axiom-types (~> 0.1) - coercible (~> 1.0) - descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) + unf_ext (0.0.7.7) PLATFORMS ruby DEPENDENCIES + actionpack activesupport addressable border_patrol diff --git a/server.ru b/server.ru index 5ec2835..fcf555a 100644 --- a/server.ru +++ b/server.ru @@ -24,6 +24,7 @@ require './api/root' require 'rack/cors' require 'rack/contrib/locale' require 'rack/contrib/try_static' +require 'action_dispatch/middleware/remote_ip.rb' use Rack::ServerPages do |config| config.view_path = 'public' @@ -47,3 +48,5 @@ use Rack::TryStatic, root: 'public', urls: %w[/], try: ['.html', 'route.html', '/route.html', 'isoline.html', '/isoline.html'] + +use ActionDispatch::RemoteIp From 17a3a4006e7c10f9b71f53bb95f6b47abb87197f Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 5 Feb 2021 15:41:35 +0100 Subject: [PATCH 04/14] Expire at on api keys --- api/v01/api.rb | 8 +++++++- config/access.rb | 1 + test/api/root_test.rb | 2 +- test/api/v01/api_test.rb | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/api/v01/api_test.rb diff --git a/api/v01/api.rb b/api/v01/api.rb index 9af4447..f2e9300 100644 --- a/api/v01/api.rb +++ b/api/v01/api.rb @@ -23,6 +23,8 @@ require './api/v01/isoline' require './api/v01/capability' +require 'active_support/core_ext/string/conversions' + require 'date' class QuotaExceeded < StandardError @@ -38,7 +40,11 @@ module Api module V01 class Api < Grape::API before do - error!('401 Unauthorized', 401) unless params && RouterWrapper.access(true).keys.include?(params[:api_key]) + if !params || !RouterWrapper.access(true).key?(params[:api_key]) + error!('401 Unauthorized', 401) + elsif RouterWrapper.access[params[:api_key]][:expire_at]&.to_date&.send(:<, Date.today) + error!('402 Subscription expired', 402) + end end helpers do diff --git a/config/access.rb b/config/access.rb index 00588d5..3bccbf6 100644 --- a/config/access.rb +++ b/config/access.rb @@ -26,5 +26,6 @@ module RouterWrapper { operation: :matrix, daily: 2 }, { monthly: 4 } ]}, + 'expired' => { profile: :standard, expire_at: '2000-01-01' } } end diff --git a/test/api/root_test.rb b/test/api/root_test.rb index bf3f76b..9d9b555 100644 --- a/test/api/root_test.rb +++ b/test/api/root_test.rb @@ -27,7 +27,7 @@ def app end def test_ping - get '/ping?api_key=demo' + get '/ping' assert last_response.ok?, last_response.body assert_equal '"pong"', last_response.body end diff --git a/test/api/v01/api_test.rb b/test/api/v01/api_test.rb new file mode 100644 index 0000000..130e52a --- /dev/null +++ b/test/api/v01/api_test.rb @@ -0,0 +1,38 @@ +# Copyright © Mapotempo, 2020 +# +# This file is part of Mapotempo. +# +# Mapotempo is free software. You can redistribute it and/or +# modify since you respect the terms of the GNU Affero General +# Public License as published by the Free Software Foundation, +# either version 3 of the License, or (at your option) any later version. +# +# Mapotempo is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Mapotempo. If not, see: +# +# +require './test/test_helper' + +class Api::V01::ApiTest < Minitest::Test + include Rack::Test::Methods + + def app + Api::Root + end + + def test_should_not_access + get '/0.1/route', {loc: '43.2804,5.3806,43.291576,5.355835'} + assert_equal 401, last_response.status + assert_equal '401 Unauthorized', JSON.parse(last_response.body)['error'] + end + + def test_should_not_access_if_expired + get '/0.1/route', {api_key: 'expired', loc: '43.2804,5.3806,43.291576,5.355835'} + assert_equal 402, last_response.status + assert_equal '402 Subscription expired', JSON.parse(last_response.body)['error'] + end +end From 412ee9259118b58c2518eada2b5561a2c22d346a Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Wed, 2 Jun 2021 16:03:48 +0200 Subject: [PATCH 05/14] Manage map tiles and router depending of the api key --- public/js/{geocode.js.erb => geocode.js} | 6 +++++- public/route.html.erb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) rename public/js/{geocode.js.erb => geocode.js} (96%) diff --git a/public/js/geocode.js.erb b/public/js/geocode.js similarity index 96% rename from public/js/geocode.js.erb rename to public/js/geocode.js index e14a502..d11466a 100644 --- a/public/js/geocode.js.erb +++ b/public/js/geocode.js @@ -1,8 +1,12 @@ +function getApiKey() { + return document.location.search.split('api_key=')[1] || 'demo'; +} + var options = { betaUrl: "https://geocode.beta.mapotempo.com/0.1/geocode.json", prodUrl: "https://geocode.mapotempo.com/0.1/geocode.json", serviceUrl: "0.1", - apiKey: "<%= params['api_key'] || 'demo' %>" + apiKey: getApiKey() }; var map = L.mapotempo.map('map').setView([44.837778, -0.579197], 13); diff --git a/public/route.html.erb b/public/route.html.erb index 9ada7d7..d8bd3ab 100644 --- a/public/route.html.erb +++ b/public/route.html.erb @@ -34,7 +34,7 @@ - + From 67da60fc6affa9a63ddc7eddcb6bdf049d6a15ad Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Thu, 22 Jul 2021 16:14:32 +0200 Subject: [PATCH 06/14] Better message on OTP InternalError --- wrappers/otp.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/wrappers/otp.rb b/wrappers/otp.rb index cb2d237..b22284f 100644 --- a/wrappers/otp.rb +++ b/wrappers/otp.rb @@ -63,12 +63,18 @@ def route(locs, dimension, departure, arrival, language, with_geometry, options wheelchair: false, showIntermediateStops: false } - request = RestClient.get(@url + '/otp/routers/' + @router_id + '/plan', { - accept: :json, - params: params - }) - data = JSON.parse(request) - @cache.write(key, data) if data && !data['error'] && data['plan'] && data['plan']['itineraries'] + + begin + request = RestClient.get(@url + '/otp/routers/' + @router_id + '/plan', { + accept: :json, + params: params + }) + data = JSON.parse(request) + @cache.write(key, data) if data && !data['error'] && data['plan'] && data['plan']['itineraries'] + rescue RestClient::ExceptionWithResponse => e + e.message += " - OTP #{@router_id}" + raise e + end end ret = { From f3d862e7389d4f792bb16f7947675150ef7c870c Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Thu, 29 Jul 2021 09:51:42 +0200 Subject: [PATCH 07/14] Add error message on isolines --- wrappers/osrm.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/wrappers/osrm.rb b/wrappers/osrm.rb index 2bd8067..7c85234 100644 --- a/wrappers/osrm.rb +++ b/wrappers/osrm.rb @@ -254,11 +254,16 @@ def isoline(loc, dimension, size, _departure, language, options = {}) approaches: options[:approach] == :curb ? (['curb'] * loc.size).join(';') : nil, exclude: [options[:toll] == false ? 'toll' : nil, options[:motorway] == false ? 'motorway' : nil, options[:track] == false ? 'track' : nil].compact.join(','), }.delete_if { |k, v| v.nil? || v == '' } - request = RestClient.get(@url_isoline[dimension] + '/0.1/isochrone', { - accept: :json, - params: params - }) - @cache.write(key, request.body) + begin + request = RestClient.get(@url_isoline[dimension] + '/0.1/isochrone', { + accept: :json, + params: params + }) + @cache.write(key, request.body) + rescue RestClient::ExceptionWithResponse => e + e.message += " - #{@url_isoline}" + raise e + end end if request From 18f1a5db406f1c1b21289d09d32528fecfd52505 Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 3 Sep 2021 11:47:12 +0200 Subject: [PATCH 08/14] Managing timeout, especially on matrices --- wrappers/osrm.rb | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/wrappers/osrm.rb b/wrappers/osrm.rb index 7c85234..fa64c33 100644 --- a/wrappers/osrm.rb +++ b/wrappers/osrm.rb @@ -26,6 +26,10 @@ module Wrappers class Osrm < Wrapper + TIMEOUT_DEFAULT_OPEN ||= 3 + TIMEOUT_DEFAULT ||= 60 + TIMEOUT_DEFAULT_MATRIX ||= 90 # OSRM takes 55sec for a 5kx5k matrix 1000km diagonal + def initialize(cache, hash = {}) super(cache, hash) @url_trace = { @@ -104,10 +108,13 @@ def route(locs, dimension, _departure, _arrival, language, with_geometry, option ].compact.join(','), }.delete_if { |k, v| v.nil? || v == '' } coordinates = locs.collect{ |loc| ['%f' % loc[1], '%f' % loc[0]].join(',') }.join(';') - request = RestClient.get(@url_trace[dimension] + '/route/v1/driving/' + coordinates, { - accept: :json, - params: params - }) { |response, request, result, &block| + request = RestClient::Request.execute( + method: :get, + url: @url_trace[dimension] + '/route/v1/driving/' + coordinates, + open_timeout: TIMEOUT_DEFAULT_OPEN, + read_timeout: TIMEOUT_DEFAULT, + payload: { accept: :json, params: params } + ) { |response, request, result, &block| case response.code when 200, 400 response @@ -201,10 +208,13 @@ def matrix(srcs, dsts, dimension, _departure, _arrival, language, options = {}) else uri = ::Addressable::URI.parse(@url_matrix[dim1]) uri.path = '/table/v1/driving/polyline(' + Polylines::Encoder.encode_points(locs_uniq, 1e5) + ')' - request = RestClient.get(uri.normalize.to_str, { - accept: :json, - params: params.delete_if { |k, v| v.nil? || v == '' } - }) { |response, request, result, &block| + request = RestClient::Request.execute( + method: :get, + url: uri.normalize.to_str, + open_timeout: TIMEOUT_DEFAULT_OPEN, + read_timeout: TIMEOUT_DEFAULT_MATRIX, + payload: { accept: :json, params: params.delete_if { |k, v| v.nil? || v == '' } } + ) { |response, request, result, &block| case response.code when 200, 400 response @@ -255,10 +265,13 @@ def isoline(loc, dimension, size, _departure, language, options = {}) exclude: [options[:toll] == false ? 'toll' : nil, options[:motorway] == false ? 'motorway' : nil, options[:track] == false ? 'track' : nil].compact.join(','), }.delete_if { |k, v| v.nil? || v == '' } begin - request = RestClient.get(@url_isoline[dimension] + '/0.1/isochrone', { - accept: :json, - params: params - }) + request = RestClient::Request.execute( + method: :get, + url: @url_isoline[dimension] + '/0.1/isochrone', + open_timeout: TIMEOUT_DEFAULT_OPEN, + read_timeout: TIMEOUT_DEFAULT, + payload: { accept: :json, params: params } + ) @cache.write(key, request.body) rescue RestClient::ExceptionWithResponse => e e.message += " - #{@url_isoline}" From 7f552aaa9d8a17c435a25a5e25bac426665ec9ee Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Fri, 3 Sep 2021 11:47:12 +0200 Subject: [PATCH 09/14] Managing timeout, especially on matrices --- wrappers/osrm.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/wrappers/osrm.rb b/wrappers/osrm.rb index fa64c33..30ad5c6 100644 --- a/wrappers/osrm.rb +++ b/wrappers/osrm.rb @@ -110,10 +110,10 @@ def route(locs, dimension, _departure, _arrival, language, with_geometry, option coordinates = locs.collect{ |loc| ['%f' % loc[1], '%f' % loc[0]].join(',') }.join(';') request = RestClient::Request.execute( method: :get, - url: @url_trace[dimension] + '/route/v1/driving/' + coordinates, + url: "#{@url_trace[dimension]}/route/v1/driving/#{coordinates}?#{params.to_query}", open_timeout: TIMEOUT_DEFAULT_OPEN, read_timeout: TIMEOUT_DEFAULT, - payload: { accept: :json, params: params } + payload: { accept: :json } ) { |response, request, result, &block| case response.code when 200, 400 @@ -210,10 +210,10 @@ def matrix(srcs, dsts, dimension, _departure, _arrival, language, options = {}) uri.path = '/table/v1/driving/polyline(' + Polylines::Encoder.encode_points(locs_uniq, 1e5) + ')' request = RestClient::Request.execute( method: :get, - url: uri.normalize.to_str, + url: "#{uri.normalize.to_str}?#{params.delete_if { |k, v| v.nil? || v == '' }.to_query}", open_timeout: TIMEOUT_DEFAULT_OPEN, read_timeout: TIMEOUT_DEFAULT_MATRIX, - payload: { accept: :json, params: params.delete_if { |k, v| v.nil? || v == '' } } + payload: { accept: :json } ) { |response, request, result, &block| case response.code when 200, 400 @@ -267,10 +267,10 @@ def isoline(loc, dimension, size, _departure, language, options = {}) begin request = RestClient::Request.execute( method: :get, - url: @url_isoline[dimension] + '/0.1/isochrone', + url: "#{@url_isoline[dimension]}/0.1/isochrone?#{params.to_query}", open_timeout: TIMEOUT_DEFAULT_OPEN, read_timeout: TIMEOUT_DEFAULT, - payload: { accept: :json, params: params } + payload: { accept: :json } ) @cache.write(key, request.body) rescue RestClient::ExceptionWithResponse => e From be48b9ce95d9bf27d4d55aa4cfc77c27f611e3af Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Thu, 16 Sep 2021 15:55:56 +0200 Subject: [PATCH 10/14] HERE: Return more details on unreachable point --- wrappers/here.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrappers/here.rb b/wrappers/here.rb index 707511e..b47047d 100644 --- a/wrappers/here.rb +++ b/wrappers/here.rb @@ -327,7 +327,7 @@ def get(url_base, object, params = {}) elsif additional_data.include?({ 'key' => 'error_code', 'value' => 'NGEO_ERROR_ROUTING_CANCELLED' }) return elsif additional_data.include?({ 'key' => 'error_code', 'value' => 'NGEO_ERROR_ROUTE_NO_START_POINT' }) - raise UnreachablePointError + raise UnreachablePointError.new(error), "Here, UnreachablePoint: #{params.keys.grep(/waypoint/).map{|key| params[key]}}" elsif error['subtype'] == 'InvalidInputData' raise RouterWrapper::InvalidArgumentError.new(error), "Here, #{error['subtype']}: #{error['details']} (#{additional_data.first['key']} : #{additional_data.first['value']})" elsif error['subtype'] == 'NoRouteFound' From 256bfce9e0783104c4b9582082eb2e3aa2ca5b6c Mon Sep 17 00:00:00 2001 From: Alain ANDRE Date: Tue, 16 Nov 2021 15:00:23 +0100 Subject: [PATCH 11/14] Review fixes --- api/v01/api_base.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/api/v01/api_base.rb b/api/v01/api_base.rb index 4db21d2..c305050 100644 --- a/api/v01/api_base.rb +++ b/api/v01/api_base.rb @@ -32,8 +32,6 @@ def self.profile(api_key) ## # @param obj can be a string or an array def self.count_locations(obj) - return 0 if obj.nil? - if obj.is_a? Array # route, matrix and isoline can send array like : # [[[lat, lng], [lat, lng]]] or [[lat, lng], [lat, lng]] or [lat, lng, lat, lng] @@ -52,11 +50,9 @@ def self.count_route_locations(params) end def self.count_matrix_locations(params) - if params[:dst] && params[:src] - count_locations(params[:src]) * count_locations(params[:dst]) - elsif params[:src] && !params[:dst] - count_locations(params[:src]) * count_locations(params[:src]) - end + src_size = count_locations(params[:src]) + dst_size = params[:dst] ? count_locations(params[:dst]) : src_size + src_size * dst_size end end end From 5798b3309bf61cf325b0f48c3ffb37059f6099a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 15:23:31 +0000 Subject: [PATCH 12/14] Bump puma from 5.2.1 to 5.5.1 Bumps [puma](https://github.com/puma/puma) from 5.2.1 to 5.5.1. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.2.1...v5.5.1) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dfd12b6..1cb2135 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,13 +97,13 @@ GEM mustermann-grape (1.0.1) mustermann (>= 1.0.0) netrc (0.11.0) - nio4r (2.5.5) + nio4r (2.5.8) nokogiri (1.11.1) mini_portile2 (~> 2.5.0) racc (~> 1.4) polylines (0.4.0) public_suffix (4.0.6) - puma (5.2.1) + puma (5.5.1) nio4r (~> 2.0) racc (1.5.2) rack (2.2.3) From 515ef457375ec1cb4b16a38bbfbedef235603976 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 15:24:07 +0000 Subject: [PATCH 13/14] Bump addressable from 2.7.0 to 2.8.0 Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.7.0 to 2.8.0. - [Release notes](https://github.com/sporkmonger/addressable/releases) - [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md) - [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.7.0...addressable-2.8.0) --- updated-dependencies: - dependency-name: addressable dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index dfd12b6..fc9909d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,7 +19,7 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) ansi (1.5.0) border_patrol (0.2.1) From 93f1431fbe187fd9737aa62b4f782948a01469db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Nov 2021 15:30:19 +0000 Subject: [PATCH 14/14] Bump nokogiri from 1.11.7 to 1.12.5 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.11.7 to 1.12.5. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.11.7...v1.12.5) --- updated-dependencies: - dependency-name: nokogiri dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f960737..552ac4f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2021.0212) - mini_portile2 (2.5.0) + mini_portile2 (2.6.1) minitest (5.14.3) minitest-focus (1.2.1) minitest (>= 4, < 6) @@ -98,14 +98,14 @@ GEM mustermann (>= 1.0.0) netrc (0.11.0) nio4r (2.5.8) - nokogiri (1.11.1) - mini_portile2 (~> 2.5.0) + nokogiri (1.12.5) + mini_portile2 (~> 2.6.1) racc (~> 1.4) polylines (0.4.0) public_suffix (4.0.6) puma (5.5.1) nio4r (~> 2.0) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rack-accept (0.4.5) rack (>= 0.4)