├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.md ├── Rakefile ├── Vagrantfile ├── apitools-1.0-0.rockspec ├── middleware ├── 404-alert │ ├── 404_alert.lua │ ├── 404_alert_spec.lua │ └── apitools.json ├── 500-bitbucket-issue │ ├── 500_bitbucket_issue.lua │ ├── 500_bitbucket_issue_spec.lua │ ├── README.md │ └── apitools.json ├── 500-github-issue │ ├── 500_github_issue.lua │ ├── 500_github_issue_spec.lua │ ├── README.md │ └── apitools.json ├── 500-webhook │ ├── 500_webhook.lua │ ├── 500_webhook_spec.lua │ ├── README.md │ └── apitools.json ├── accept-encoding-identity │ ├── accept-encoding-identity.lua │ ├── accept-encoding-identity_spec.lua │ └── apitools.json ├── add-args │ ├── add_args.lua │ ├── add_args_spec.lua │ └── apitools.json ├── add-keys │ ├── add_keys.lua │ ├── add_keys_spec.lua │ └── apitools.json ├── bicing-distance │ ├── apitools.json │ ├── bicing_distance.lua │ └── bicing_distance_spec.lua ├── bicing-json │ ├── apitools.json │ ├── bicing_json.lua │ └── bicing_json_spec.lua ├── bicing-walking-distance │ ├── apitools.json │ ├── bicing_walking_distance.lua │ └── bicing_walking_distance_spec.lua ├── burningman-categorize │ ├── README.md │ ├── apitools.json │ ├── burningman_categorize.lua │ └── burningman_categorize_spec.lua ├── burningman-demultiply │ ├── README.md │ ├── apitools.json │ ├── burningman_demultiply.lua │ └── burningman_demultiply_spec.lua ├── cache-middleware │ ├── apitools.json │ ├── cache.lua │ └── cache_spec.lua ├── citybikeAPI-distance │ ├── README.md │ ├── apitools.json │ ├── citybike_location.lua │ └── citybike_location_spec.lua ├── citybikeAPI │ ├── README.md │ ├── apitools.json │ ├── citybike_payload.lua │ └── citybike_payload_spec.lua ├── cors │ ├── apitools.json │ ├── cors.lua │ └── cors_spec.lua ├── github-webhook-keen │ ├── README.md │ ├── apitools.json │ └── github-webhook-keen-io.lua ├── http-cache │ ├── README.md │ ├── apitools.json │ ├── http_cache.lua │ └── http_cache_spec.lua ├── ifg-filter-category │ ├── README.md │ ├── apitools.json │ ├── ifg_filter_rewards_by_category.lua │ └── ifg_filter_rewards_by_category_spec.lua ├── ifg-filter-price │ ├── README.md │ ├── apitools.json │ ├── ifg_filter_rewards_by_price.lua │ └── ifg_filter_rewards_by_price_spec.lua ├── measure-x-ratelimit-remaining │ ├── apitools.json │ ├── measure_x_ratelimit_remaining.lua │ └── measure_x_ratelimit_remaining_spec.lua ├── privacy │ ├── apitools.json │ ├── privacy.lua │ └── privacy_spec.lua ├── response-size-metric │ ├── apitools.json │ ├── response-size-metric.lua │ └── response-size-metric_spec.lua ├── response-time-alert │ ├── apitools.json │ ├── response_time_alert.lua │ └── response_time_alert_spec.lua ├── send-res-size-keen │ ├── README.md │ ├── apitools.json │ └── send_res_size_keen.lua ├── slow-request-github-issue │ ├── README.md │ ├── apitools.json │ ├── slow_request_github_issue.lua │ └── slow_request_github_issue_spec.lua ├── twitter-oauth │ ├── apitools.json │ ├── twitter_oauth.lua │ └── twitter_oauth_spec.lua ├── uber-alert │ ├── README.md │ ├── apitools.json │ ├── uber_pricealert.lua │ └── uber_pricealert_spec.lua ├── uber-passinfo │ ├── README.md │ ├── apitools.json │ ├── uber_passinfo.lua │ └── uber_passinfo_spec.lua ├── xml-to-json │ ├── apitools.json │ ├── xml-to-json.lua │ └── xml-to-json_spec.lua └── yo-api │ ├── README.md │ ├── apitools.json │ ├── yo.lua │ └── yo_spec.lua └── spec ├── assert_contains.lua ├── assert_difference.lua ├── env ├── bucket.lua ├── console.lua ├── env.lua ├── http.lua ├── http_utils.lua ├── inspect.lua ├── metric.lua ├── querystring.lua ├── send.lua ├── sha.lua └── time.lua ├── sandbox.lua └── spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | Gemfile.lock 3 | .vagrant 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.2 4 | cache: 5 | bundler: true 6 | apt: true 7 | directories: 8 | - /usr/local 9 | env: 10 | global: 11 | - LUAROCKS_VERSION=2.2.0 12 | - OPENRESTY_VERSION=1.7.2.1 13 | - OPENRESTY=/usr/local/openresty 14 | - PATH=$PATH:/opt/apitools-monitor/embedded/bin 15 | before_install: 16 | - curl -L -q https://packagecloud.io/install/repositories/APItools/monitor/script.deb | sudo bash 17 | - sudo apt-get -y install make unzip apitools-monitor 18 | install: 19 | - bundle install 20 | - make dependencies 21 | script: make 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'apitools-middleware', '~> 0.1.0' 3 | gem 'rake' 4 | gem 'highline' 5 | gem 'activesupport' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.1.5) 5 | i18n (~> 0.6, >= 0.6.9) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.1) 9 | tzinfo (~> 1.1) 10 | apitools-middleware (0.1.0) 11 | semantic 12 | highline (1.6.21) 13 | i18n (0.6.11) 14 | json (1.8.1) 15 | minitest (5.4.0) 16 | rake (10.3.2) 17 | semantic (1.3.0) 18 | thread_safe (0.3.4) 19 | tzinfo (1.2.2) 20 | thread_safe (~> 0.1) 21 | 22 | PLATFORMS 23 | ruby 24 | 25 | DEPENDENCIES 26 | activesupport 27 | apitools-middleware (~> 0.1.0) 28 | highline 29 | rake 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 3scale Networks S. L. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all_lua_files = $(wildcard middleware/**/*.lua) 2 | source_files = $(filter-out %_spec.lua, $(all_lua_files)) 3 | specs = $(wildcard middleware/**/*_spec.lua) 4 | pipeline_globals = console inspect log base64 hmac http bucket send time metric trace json xml 5 | MIDDLEWARE = $(patsubst middleware/%,%,$(wildcard middleware/*)) 6 | 7 | LUA_BINARIES := lua5.1 lua-5.1 luajit lua 8 | LUA := $(firstword $(foreach bin,$(LUA_BINARIES),$(shell which $(bin)))) 9 | LUAROCKS := $(shell which luarocks) 10 | LUAROCKS_VERSION := $(shell $(LUAROCKS) 2> /dev/null | grep -o 'LuaRocks [0-9]\.[0-9]\.[0-9]') 11 | 12 | PATH := $(HOME)/.luarocks/bin:$(PATH) 13 | 14 | ifneq (,$(LUA)) 15 | # Because Lua 5.1 outputs the version to stderr, where Luajit to stdout 16 | LUA_VERSION := $(shell $(LUA) -v 2>&1 | awk '{ print $$1, $$2}') 17 | endif 18 | 19 | .PHONY: all test check_specs check_sources middleware dependencies install apitools 20 | .DEFAULT_GOAL: all 21 | 22 | all: check test apitools 23 | check: check_sources check_specs 24 | 25 | luarocks: lua 26 | ifeq (,$(LUAROCKS)) 27 | @echo No luarocks found 28 | exit 1 29 | endif 30 | @echo $(LUAROCKS_VERSION) 31 | ifeq (,$(findstring 2.2,$(LUAROCKS_VERSION))) 32 | @echo "Need LuaRocks 2.2)" 33 | exit 1 34 | endif 35 | 36 | lua: 37 | ifndef LUA_VERSION 38 | @echo No Lua found. 39 | exit 1 40 | endif 41 | ifneq (,$(findstring LuaJIT,$(LUA_VERSION))) 42 | @echo Found Lua binary: $(LUA) 43 | @echo Version: $(LUA_VERSION) 44 | @echo 45 | LUA_FOUND=1 46 | endif 47 | ifneq (,$(findstring 5.1,$(LUA_VERSION))) 48 | @echo Found Lua binary: $(LUA) 49 | @echo Version: $(LUA_VERSION) 50 | @echo 51 | LUA_FOUND=1 52 | endif 53 | ifndef LUA_FOUND 54 | @echo $(LUA) is not supported version 55 | exit 1 56 | endif 57 | 58 | test: busted 59 | busted -v middleware 60 | @echo 61 | 62 | check_sources: luacheck 63 | luacheck -q -a $(source_files) --globals - $(pipeline_globals) 64 | @echo 65 | 66 | check_specs: luacheck 67 | luacheck -q -a $(specs) --globals - describe it pending before_each $(pipeline_globals) 68 | @echo 69 | 70 | apitools: 71 | bundle exec rake test 72 | @echo 73 | 74 | vagrant: 75 | vagrant up 76 | vagrant ssh -c 'cd /vagrant && make' 77 | - vagrant halt 78 | 79 | middleware: $(MIDDLEWARE) 80 | $(MIDDLEWARE): % : 81 | busted -v middleware/$@ 82 | 83 | luacheck: install 84 | busted: install 85 | 86 | install: luarocks 87 | luarocks make --local 88 | 89 | dependencies: ECHO := 1 90 | dependencies: install 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APItools Middleware Repository [![Build Status](https://travis-ci.org/APItools/middleware.svg?branch=master)](https://travis-ci.org/APItools/middleware) 2 | 3 | This is a collection of Middleware that you can use in APItools Traffic Monitors. Either [On Premise](//github.com/APItools/monitor) or [Cloud](//apitools.com). 4 | 5 | You can use almost all of [Lua Standard Library](http://www.lua.org/manual/5.1/manual.html#5) with [some exceptions](//github.com/APItools/monitor/blob/master/lua/sandbox.lua#L53-L71). 6 | 7 | ## Creating Middleware 8 | 9 | We provide simple rake task to get started with middleware template. 10 | You just have to have Ruby, install dependencies, run `rake middleware` 11 | and fill out the questions. 12 | 13 | ```shell 14 | bundle 15 | rake middleware 16 | ``` 17 | 18 | ## Testing Middleware 19 | 20 | When you generate a middleware by `rake middleware` a spec will be created for you. 21 | Then you can run all the tests by running `make`. 22 | 23 | 24 | ### Test Dependencies 25 | To run all the tests you need: `lua` (5.1) (or `luajit`) with `luarocks`. 26 | Then you can install dependencies via `make dependencies`. 27 | 28 | If you can't get it installed on your machine, you can use vagrant. 29 | Run `make vagrant` to automatically execute all tests in vagrant machine. 30 | 31 | ### OSX 32 | If you have homebrew, you can just install luarocks lika `brew install luarocks --with-luajit` 33 | and run `make dependencies`. 34 | 35 | ## Contributing 36 | 37 | 1. Fork it ( https://github.com/APItools/middleware/fork ) 38 | 2. Create your feature branch (`git checkout -b my-new-feature`) 39 | 3. Commit your changes (`git commit -am 'Add some feature'`) 40 | 4. Push to the branch (`git push origin my-new-feature`) 41 | 5. Create a new Pull Request 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'apitools-middleware' 2 | require 'highline/import' 3 | require 'active_support/core_ext/string/inflections' 4 | require 'i18n' 5 | require 'json' 6 | require 'erb' 7 | 8 | I18n.enforce_available_locales = true 9 | 10 | task default: :test 11 | 12 | task :test do 13 | repository = Apitools::Middleware::LocalRepository.new 14 | middleware = repository.middleware 15 | 16 | puts "Found #{middleware.size} middlewares." 17 | 18 | _valid, invalid = middleware.partition(&:valid?) 19 | 20 | invalid.each do |mw| 21 | warn "#{mw.path} is not valid" 22 | end 23 | 24 | if invalid.any? 25 | puts "[FAIL] #{invalid.size} middleware are invalid" 26 | exit(false) 27 | end 28 | 29 | puts '[SUCCESS] all middlewares are ok' 30 | end 31 | 32 | 33 | MIDDLEWARE_TEMPLATE = <<-TEMPLATE 34 | --[[ 35 | -- 36 | -- This middleware ... 37 | -- 38 | --]] 39 | 40 | return function(request, next_middleware) 41 | return next_middleware() 42 | end 43 | TEMPLATE 44 | 45 | README_TEMPLATE = <<-TEMPLATE 46 | # Middleware ... 47 | 48 | Usage ... 49 | 50 | ## Requirements 51 | * another middleware 52 | TEMPLATE 53 | 54 | SPEC_TEMPLATE = <<-TEMPLATE 55 | local spec = require 'spec.spec' 56 | 57 | describe('<%= name %>', function() 58 | local <%= param %> 59 | before_each(function() 60 | <%= param %> = spec.middleware('<%= [folder, file].join('/') %>') 61 | end) 62 | 63 | it('it calls and returns next middleware', function() 64 | local request = spec.request({method = 'GET', uri = '/'}) 65 | local next_middleware = spec.next_middleware(function() 66 | assert.contains(request, {method = 'GET', uri = '/'}) 67 | return {status = 200, body = 'ok'} 68 | end) 69 | 70 | local response = <%= param %>(request, next_middleware) 71 | 72 | assert.spy(next_middleware).was_called() 73 | 74 | assert.contains(response, {status = 200, body = 'ok'}) 75 | end) 76 | end) 77 | TEMPLATE 78 | 79 | def render(template, binding) 80 | ERB.new(template).result(binding) 81 | end 82 | 83 | desc 'New Middleware Wizard' 84 | task :middleware do 85 | comma_separated = ->(str) { str.split(/,\s*/).map(&:strip) } 86 | spec = { 87 | name: name = ask('Middleware Name: ', String) {|q| q.validate = /./ }, 88 | description: ask('Description: ', String) {|q| q.validate = /./ }, 89 | files: [file = ask('File Name: ', String) {|q| q.default = name.parameterize('_') + '.lua'; q.validate = /^\S+\.\S+$/ }], 90 | spec: [Pathname(file).sub_ext('_spec.lua').to_s], 91 | author: ask('Author: ', String) {|q| q.default = `git config --get user.name`.strip }, 92 | email: ask('Email: ', String) {|q| q.validate = /@/; q.default = `git config --get user.email`.strip }, 93 | version: ask('Version: ', String) {|q| q.default = '1.0.0'; q.validate = /^\d\.\d/ }, 94 | categories: ask('Categories: ', comma_separated), 95 | endpoints: ask('Endpoints: ', comma_separated) {|q| q.default = '*' }, 96 | } 97 | folder = ask('Folder: ', String) { |q| q.default = name.parameterize('-'); q.validate = /./ } 98 | 99 | path = Pathname('middleware').join(folder) 100 | 101 | path.exist? and raise "#{folder} already exists" 102 | 103 | json = path.join('apitools.json') 104 | json.exist? and raise "apitools.json already exists" 105 | 106 | lua = path.join(file) 107 | lua.exist? and raise "#{file} already exists" 108 | 109 | readme = path.join('README.md') 110 | readme.exist? and raise "#{readme} already exists" 111 | 112 | test = lua.sub_ext('_spec.lua') 113 | test.exist? and raise "#{test} already exists" 114 | 115 | name = spec.fetch(:name) 116 | param = lua.basename(lua.extname).to_s.parameterize('_') 117 | 118 | path.mkdir 119 | puts "Created #{path}" 120 | json.write(JSON.pretty_generate(spec)) 121 | puts "Created #{json}" 122 | lua.write(render(MIDDLEWARE_TEMPLATE, binding)) 123 | puts "Created #{lua}" 124 | readme.write(render(README_TEMPLATE, binding)) 125 | puts "Created #{readme}" 126 | test.write(render(SPEC_TEMPLATE, binding)) 127 | puts "Created #{test}" 128 | end 129 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure('2') do |config| 5 | config.vm.box = "ubuntu/precise64" 6 | 7 | config.vm.provision "shell", inline: <<-SCRIPT 8 | apt-add-repository ppa:brightbox/ruby-ng -y 9 | apt-get update -y 10 | apt-get -y install ruby2.1 ruby2.1-dev ruby-switch 11 | ruby-switch --set ruby2.1 12 | gem install bundler --no-rdoc --no-ri 13 | SCRIPT 14 | 15 | config.vm.provision "shell", 16 | path: 'https://gist.githubusercontent.com/mikz/411dbbc2aad5f147f87b/raw/ffb23a9c46d3210ba4b6b1067effd674b99cacdc/travis.rb', 17 | args: '/vagrant' 18 | end 19 | -------------------------------------------------------------------------------- /apitools-1.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "apitools" 2 | source = { 3 | url = '.' 4 | } 5 | version = '1.0-0' 6 | description = { 7 | } 8 | dependencies = { 9 | "lua ~> 5.1", 10 | 'luasec >= 0', 11 | 'luabitop >= 0', 12 | 'luacheck >= 0', 13 | 'busted >= 0', 14 | 'lua-cjson >= 0', 15 | 'luasocket >= 0', 16 | 'luaexpat >= 0' 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /middleware/404-alert/404_alert.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | local five_mins = 60 * 5 3 | local res = next_middleware() 4 | local last_mail = bucket.middleware.get('last_mail') 5 | if res.status == 404 and (not last_mail or last_mail < time.now() - five_mins) then 6 | send.mail('YOUR-MAIL-HERE@gmail.com', "A 404 has ocurred", 7 | "a 404 error happened in " .. request.uri_full .. ' see full trace: ' .. trace.link) 8 | bucket.middleware.set('last_mail', time.now()) 9 | end 10 | return res 11 | end 12 | 13 | -------------------------------------------------------------------------------- /middleware/404-alert/404_alert_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("404 alert", function() 4 | local alert 5 | before_each(function() 6 | alert = spec.middleware('404-alert/404_alert.lua') 7 | end) 8 | 9 | describe("when the status is not 404", function() 10 | it("does nothing", function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = alert(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | assert.contains(response, {status = 200, body = 'ok'}) 21 | 22 | assert.equal(#spec.sent.emails, 0) 23 | assert.equal(#spec.bucket.middleware.get_keys(), 0) 24 | end) 25 | end) 26 | 27 | describe("when the status is 404", function() 28 | describe("when it happens once", function() 29 | it("sends an email and marks the middleware bucket", function() 30 | local request = spec.request({uri = '/'}) 31 | local next_middleware = spec.next_middleware(function() 32 | assert.contains(request, {method = 'GET', uri = '/'}) 33 | return {status = 404, body = 'not ok'} 34 | end) 35 | 36 | local response = alert(request, next_middleware) 37 | 38 | assert.spy(next_middleware).was_called() 39 | assert.contains(response, {status = 404, body = 'not ok'}) 40 | 41 | assert.truthy(spec.bucket.middleware.get('last_mail')) 42 | 43 | assert.equal(#spec.sent.emails, 1) 44 | 45 | local last_email = spec.sent.emails.last 46 | assert.equal('YOUR-MAIL-HERE@gmail.com', last_email.to) 47 | assert.equal('A 404 has ocurred', last_email.subject) 48 | assert.equal('a 404 error happened in http://localhost/ see full trace: ', last_email.message) 49 | end) 50 | end) 51 | 52 | describe("when the 404 happens more than once", function() 53 | describe("and the time between errors is smaller than the threshold", function() 54 | it("only sends one email", function() 55 | local request = spec.request({method = 'GET', uri = '/'}) 56 | local next_middleware = spec.next_middleware(function() 57 | assert.contains(request, {method = 'GET', uri = '/'}) 58 | return {status = 404, body = 'not ok'} 59 | end) 60 | 61 | alert(request, next_middleware) 62 | spec.advance_time(10) 63 | alert(request, next_middleware) -- twice 64 | assert.spy(next_middleware).was_called(2) 65 | 66 | 67 | assert.equal(1, #spec.sent.emails) 68 | end) 69 | 70 | describe("and the time between errors is greater than the threshold", function() 71 | it("sends several emails", function() 72 | local request = spec.request({method = 'GET', uri = '/'}) 73 | local next_middleware = spec.next_middleware(function() 74 | assert.contains(request, {method = 'GET', uri = '/'}) 75 | return {status = 404, body = 'not ok'} 76 | end) 77 | 78 | alert(request, next_middleware) 79 | spec.advance_time(5 * 60 + 1) 80 | alert(request, next_middleware) 81 | assert.spy(next_middleware).was_called(2) 82 | 83 | 84 | assert.equal(2, #spec.sent.emails) 85 | end) 86 | end) 87 | end) 88 | 89 | 90 | end) 91 | 92 | 93 | 94 | 95 | end) 96 | end) 97 | -------------------------------------------------------------------------------- /middleware/404-alert/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "404 Alert", 3 | "description": "Sends an email in case a call returns a 404", 4 | "files": ["404_alert.lua"], 5 | "spec": ["404_alert_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["devops", "notifications"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/500-bitbucket-issue/500_bitbucket_issue.lua: -------------------------------------------------------------------------------- 1 | local bitbucket_username = 'BITBUCKET_USERNAME' 2 | local bitbucket_password = 'BITBUCKET_PASSWORD' 3 | local bitbucket_repo_full_name = 'BITBUCKET_REPO_FULL_NAME' 4 | 5 | -- taken from leafo/lapis 6 | -- see https://github.com/leafo/lapis/blob/v1.0.6/lapis/util.lua#L108-L110 7 | local function slugify(str) 8 | return (str:gsub("[%s_]+", "-"):gsub("[^%w%-]+", ""):gsub("-+", "-")):lower() 9 | end 10 | 11 | -- makes an authenticated request to Bitbucket API 12 | local function bitbucket_request(method, path, body) 13 | local auth = bitbucket_username .. ':' .. bitbucket_password 14 | local request = { 15 | method = method, 16 | url = 'https://bitbucket.org/api/1.0' .. path, 17 | headers = {Authorization = 'Basic ' .. base64.encode(auth)}, 18 | body = body 19 | } 20 | local response_body = http.simple(request) 21 | return json.decode(response_body) 22 | end 23 | 24 | -- makes a request to Bitbucket API to create a new issue 25 | local function create_bitbucket_issue(body) 26 | local path = '/repositories/' .. bitbucket_repo_full_name .. '/issues' 27 | return bitbucket_request('POST', path, body) 28 | end 29 | 30 | -- makes a request to Bitbucket API to update an issue 31 | local function update_bitbucket_issue(issue_number, body) 32 | local path = '/repositories/' .. bitbucket_repo_full_name .. '/issues/' .. issue_number 33 | return bitbucket_request('PUT', path, body) 34 | end 35 | 36 | return function(request, next_middleware) 37 | local response = next_middleware() 38 | 39 | if response.status == 500 then 40 | -- notify error 41 | send.notification({msg = response.body, level = 'error'}) 42 | 43 | -- response body is used as key in the middleware bucket 44 | local issue_key = slugify(response.body) 45 | local issue_number = bucket.middleware.get(issue_key) 46 | 47 | if issue_number == nil then 48 | -- create a new Bitbucket issue 49 | local issue = create_bitbucket_issue({title = request.uri, content = response.body}) 50 | 51 | -- register issue number 52 | bucket.middleware.set(issue_key, issue.local_id) 53 | else 54 | -- update the Bitbucket issue (reopen) 55 | update_bitbucket_issue(issue_number, {status = 'open'}) 56 | end 57 | end 58 | 59 | return response 60 | end 61 | -------------------------------------------------------------------------------- /middleware/500-bitbucket-issue/500_bitbucket_issue_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('500 Bitbucket issue', function() 4 | local issue 5 | before_each(function() 6 | issue = spec.middleware('500-bitbucket-issue/500_bitbucket_issue.lua') 7 | end) 8 | 9 | describe('when the status is not 500', function() 10 | it('does nothing', function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = issue(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | assert.contains(response, {status = 200, body = 'ok'}) 21 | 22 | assert.equal(#spec.sent.events, 0) 23 | assert.equal(#spec.bucket.middleware.get_keys(), 0) 24 | end) 25 | end) 26 | 27 | describe('when the status is 500', function() 28 | describe('when it happens once', function() 29 | it('sends a notification, sends a request to Bitbucket API, and marks the middleware bucket', function() 30 | local request = spec.request({uri = '/'}) 31 | local next_middleware = spec.next_middleware(function() 32 | assert.contains(request, {method = 'GET', uri = '/'}) 33 | return {status = 500, body = 'an error message'} 34 | end) 35 | 36 | spec.mock_http({ 37 | method = 'POST', 38 | url = 'https://bitbucket.org/api/1.0/repositories/BITBUCKET_REPO_FULL_NAME/issues', 39 | body = 'content=an+error+message&title=%2F', 40 | headers = {Authorization = 'Basic QklUQlVDS0VUX1VTRVJOQU1FOkJJVEJVQ0tFVF9QQVNTV09SRA=='} 41 | }, { 42 | body = '{"resource_uri":"/1.0/repositories/owner/repo/issues/1","local_id":1}' 43 | }) 44 | 45 | local response = issue(request, next_middleware) 46 | 47 | assert.spy(next_middleware).was_called() 48 | assert.contains(response, {status = 500, body = 'an error message'}) 49 | 50 | assert.equal(1, #spec.sent.events) 51 | assert.truthy(spec.bucket.middleware.get('an-error-message')) 52 | 53 | local last_event = spec.sent.events.last 54 | assert.same({channel = 'middleware', level = 'error', msg = 'an error message'}, last_event) 55 | end) 56 | end) 57 | 58 | describe('when it happens more than once', function() 59 | it('sends two notifications and reopens Bitbucket issue', function() 60 | local request = spec.request({method = 'GET', uri = '/'}) 61 | local next_middleware = spec.next_middleware(function() 62 | assert.contains(request, {method = 'GET', uri = '/'}) 63 | return {status = 500, body = 'an error message'} 64 | end) 65 | 66 | spec.mock_http({ 67 | method = 'POST', 68 | url = 'https://bitbucket.org/api/1.0/repositories/BITBUCKET_REPO_FULL_NAME/issues', 69 | body = 'content=an+error+message&title=%2F', 70 | headers = {Authorization = 'Basic QklUQlVDS0VUX1VTRVJOQU1FOkJJVEJVQ0tFVF9QQVNTV09SRA=='} 71 | }, { 72 | body = '{"resource_uri":"/1.0/repositories/owner/repo/issues/1","local_id":1}' 73 | }) 74 | 75 | spec.mock_http({ 76 | method = 'PUT', 77 | url = 'https://bitbucket.org/api/1.0/repositories/BITBUCKET_REPO_FULL_NAME/issues/1', 78 | body = 'status=open', 79 | headers = {Authorization = 'Basic QklUQlVDS0VUX1VTRVJOQU1FOkJJVEJVQ0tFVF9QQVNTV09SRA=='} 80 | }, { 81 | body = '{"resource_uri":"/1.0/repositories/owner/repo/issues/1","local_id":1}' 82 | }) 83 | 84 | issue(request, next_middleware) 85 | issue(request, next_middleware) -- twice 86 | assert.spy(next_middleware).was_called(2) 87 | 88 | assert.equal(2, #spec.sent.events) 89 | end) 90 | end) 91 | 92 | end) 93 | end) 94 | -------------------------------------------------------------------------------- /middleware/500-bitbucket-issue/README.md: -------------------------------------------------------------------------------- 1 | # 500 Bitbucket issue middleware 2 | 3 | Open a Bitbucket issue when an error occurs. If an error occurs more than once the middleware updates the associated issue by reopening it. 4 | 5 | ## Usage 6 | 7 | 1. Change `BITBUCKET_USERNAME` and `BITBUCKET_PASSWORD` in `500_bitbucket_issue.lua` with your Bitbucket username and password. 8 | 1. Change `BITBUCKET_REPO_FULL_NAME` in `500_bitbucket_issue.lua` with your Bitbucket repository full name, e.g. 'leafo/lapis'. 9 | -------------------------------------------------------------------------------- /middleware/500-bitbucket-issue/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "500 Bitbucket issue", 3 | "description": "Open a Bitbucket issue when an error occurs", 4 | "files": [ 5 | "500_bitbucket_issue.lua" 6 | ], 7 | "spec": [ 8 | "500_bitbucket_issue_spec.lua" 9 | ], 10 | "author": "Giovanni Cappellotto", 11 | "email": "potomak84@gmail.com", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "devops", 15 | "notifications", 16 | "bitbucket" 17 | ], 18 | "endpoints": [ 19 | "*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /middleware/500-github-issue/500_github_issue.lua: -------------------------------------------------------------------------------- 1 | local github_access_token = 'GITHUB_ACCESS_TOKEN' 2 | local github_repo_full_name = 'GITHUB_REPO_FULL_NAME' 3 | 4 | -- taken from leafo/lapis 5 | -- see https://github.com/leafo/lapis/blob/v1.0.6/lapis/util.lua#L108-L110 6 | local function slugify(str) 7 | return (str:gsub("[%s_]+", "-"):gsub("[^%w%-]+", ""):gsub("-+", "-")):lower() 8 | end 9 | 10 | -- makes an authenticated request to GitHub API 11 | local function github_request(method, path, body) 12 | local request = { 13 | method = method, 14 | url = 'https://api.github.com' .. path, 15 | headers = {Authorization = 'token ' .. github_access_token} 16 | } 17 | local response_body = http.simple(request, json.encode(body)) 18 | return json.decode(response_body) 19 | end 20 | 21 | -- makes a request to GitHub API to create a new issue 22 | local function create_github_issue(body) 23 | local path = '/repos/' .. github_repo_full_name .. '/issues' 24 | return github_request('POST', path, body) 25 | end 26 | 27 | -- makes a request to GitHub API to update an issue 28 | local function update_github_issue(issue_number, body) 29 | local path = '/repos/' .. github_repo_full_name .. '/issues/' .. issue_number 30 | return github_request('PATCH', path, body) 31 | end 32 | 33 | return function(request, next_middleware) 34 | local response = next_middleware() 35 | 36 | if response.status == 500 then 37 | -- notify error 38 | send.notification({msg = response.body, level = 'error'}) 39 | 40 | -- response body is used as key in the middleware bucket 41 | local issue_key = slugify(response.body) 42 | local issue_number = bucket.middleware.get(issue_key) 43 | 44 | if issue_number == nil then 45 | -- create a new GitHub issue 46 | local issue = create_github_issue({title = response.body}) 47 | 48 | -- register issue number 49 | bucket.middleware.set(issue_key, issue.number) 50 | else 51 | -- update the GitHub issue (reopen) 52 | update_github_issue(issue_number, {state = 'open'}) 53 | end 54 | end 55 | 56 | return response 57 | end 58 | -------------------------------------------------------------------------------- /middleware/500-github-issue/500_github_issue_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('500-github-issue', function() 4 | local issue 5 | before_each(function() 6 | issue = spec.middleware('500-github-issue/500_github_issue.lua') 7 | end) 8 | 9 | describe('when the status is not 500', function() 10 | it('does nothing', function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = issue(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | assert.contains(response, {status = 200, body = 'ok'}) 21 | 22 | assert.equal(#spec.sent.events, 0) 23 | assert.equal(#spec.bucket.middleware.get_keys(), 0) 24 | end) 25 | end) 26 | 27 | describe('when the status is 500', function() 28 | describe('when it happens once', function() 29 | it('sends a notification, sends a request to GitHub API, and marks the middleware bucket', function() 30 | local request = spec.request({uri = '/'}) 31 | local next_middleware = spec.next_middleware(function() 32 | assert.contains(request, {method = 'GET', uri = '/'}) 33 | return {status = 500, body = 'an error message'} 34 | end) 35 | 36 | spec.mock_http({ 37 | method = 'POST', 38 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues', 39 | body = '{"title":"an error message"}', 40 | headers = {Authorization = 'token GITHUB_ACCESS_TOKEN'} 41 | }, { 42 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 43 | }) 44 | 45 | local response = issue(request, next_middleware) 46 | 47 | assert.spy(next_middleware).was_called() 48 | assert.contains(response, {status = 500, body = 'an error message'}) 49 | 50 | assert.equal(1, #spec.sent.events) 51 | assert.truthy(spec.bucket.middleware.get('an-error-message')) 52 | 53 | local last_event = spec.sent.events.last 54 | assert.same({channel = 'middleware', level = 'error', msg = 'an error message'}, last_event) 55 | end) 56 | end) 57 | 58 | describe('when it happens more than once', function() 59 | it('sends two notifications and reopens GitHub issue', function() 60 | local request = spec.request({method = 'GET', uri = '/'}) 61 | local next_middleware = spec.next_middleware(function() 62 | assert.contains(request, {method = 'GET', uri = '/'}) 63 | return {status = 500, body = 'an error message'} 64 | end) 65 | 66 | spec.mock_http({ 67 | method = 'POST', 68 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues', 69 | body = '{"title":"an error message"}', 70 | headers = { Authorization = 'token GITHUB_ACCESS_TOKEN' } 71 | }, { 72 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 73 | }) 74 | 75 | spec.mock_http({ 76 | method = 'POST', -- FIXME: this really should be PATCH, but http.simple is broken 77 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues/1', 78 | body = '{"state":"open"}', 79 | headers = { Authorization = 'token GITHUB_ACCESS_TOKEN' } 80 | }, { 81 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 82 | }) 83 | 84 | issue(request, next_middleware) 85 | issue(request, next_middleware) -- twice 86 | assert.spy(next_middleware).was_called(2) 87 | 88 | assert.equal(2, #spec.sent.events) 89 | end) 90 | end) 91 | 92 | end) 93 | end) 94 | -------------------------------------------------------------------------------- /middleware/500-github-issue/README.md: -------------------------------------------------------------------------------- 1 | # 500 GitHub issue middleware 2 | 3 | Open a Github issue when an error occurs. If an error occurs more than once the middleware updates the associated issue by reopening it. 4 | 5 | ## Usage 6 | 7 | 1. Change `GITHUB_ACCESS_TOKEN` in `500_github_issue.lua` with your GitHub API access token. 8 | 1. Change `GITHUB_REPO_FULL_NAME` in `500_github_issue.lua` with your GitHub repository full name, e.g. 'leafo/lapis'. 9 | -------------------------------------------------------------------------------- /middleware/500-github-issue/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "500 GitHub issue", 3 | "description": "Open a Github issue when an error occurs.", 4 | "files": [ 5 | "500_github_issue.lua" 6 | ], 7 | "spec": [ 8 | "500_github_issue_spec.lua" 9 | ], 10 | "author": "Giovanni Cappellotto", 11 | "email": "potomak84@gmail.com", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "devops", 15 | "notifications", 16 | "github" 17 | ], 18 | "endpoints": [ 19 | "*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /middleware/500-webhook/500_webhook.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | local webhook_url = 'http://example.org/' 3 | 4 | local response = next_middleware() 5 | 6 | if response.status == 500 then 7 | -- send POST request to webhook 8 | http.simple{method = 'POST', url = webhook_url, body = {error = response.body}} 9 | 10 | -- register webhook request 11 | bucket.middleware.set('last_webhook_request', time.now()) 12 | end 13 | 14 | return response 15 | end 16 | -------------------------------------------------------------------------------- /middleware/500-webhook/500_webhook_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('500 Webhook', function() 4 | local webhook 5 | before_each(function() 6 | webhook = spec.middleware('500-webhook/500_webhook.lua') 7 | end) 8 | 9 | describe('when the status is not 500', function() 10 | it('does nothing', function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = webhook(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | assert.contains(response, {status = 200, body = 'ok'}) 21 | 22 | assert.equal(0, #spec.bucket.middleware.get_keys()) 23 | end) 24 | end) 25 | 26 | describe('when the status is 500', function() 27 | it('sends a request to webhook and marks the middleware bucket', function() 28 | local request = spec.request({method = 'GET', uri = '/'}) 29 | local next_middleware = spec.next_middleware(function() 30 | assert.contains(request, {method = 'GET', uri = '/'}) 31 | return {status = 500, body = 'an error message'} 32 | end) 33 | 34 | spec.mock_http({ 35 | method = 'POST', 36 | url = 'http://example.org/', 37 | body = 'error=an+error+message' 38 | }, {}) 39 | 40 | local response = webhook(request, next_middleware) 41 | 42 | assert.spy(next_middleware).was_called() 43 | assert.contains(response, {status = 500, body = 'an error message'}) 44 | 45 | assert.truthy(spec.bucket.middleware.get('last_webhook_request')) 46 | end) 47 | end) 48 | 49 | end) 50 | -------------------------------------------------------------------------------- /middleware/500-webhook/README.md: -------------------------------------------------------------------------------- 1 | # 500 Webhook middleware 2 | 3 | Send a POST request to URL when an error occurs. 4 | 5 | ## Usage 6 | 7 | 1. Change `WEBHOOK_URL` in `500_webhook.lua` with your Webhook URL. 8 | -------------------------------------------------------------------------------- /middleware/500-webhook/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "500 Webhook", 3 | "description": "Send a POST request to URL when an error occurs", 4 | "files": [ 5 | "500_webhook.lua" 6 | ], 7 | "spec": [ 8 | "500_webhook_spec.lua" 9 | ], 10 | "author": "Giovanni Cappellotto", 11 | "email": "potomak84@gmail.com", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "notifications", 15 | "devops" 16 | ], 17 | "endpoints": [ 18 | "*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /middleware/accept-encoding-identity/accept-encoding-identity.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | request.headers['Accept-Encoding'] = 'identity' 3 | return next_middleware() 4 | end 5 | 6 | -------------------------------------------------------------------------------- /middleware/accept-encoding-identity/accept-encoding-identity_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("Accept encoding identity", function() 4 | it("adds a header", function() 5 | local accept = spec.middleware('accept-encoding-identity/accept-encoding-identity.lua') 6 | local request = spec.request({ method = 'GET', uri = '/'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, {method = 'GET', uri = '/', headers = {['Accept-Encoding'] = 'identity'}}) 9 | return {status = 200, body = 'ok'} 10 | end) 11 | 12 | local response = accept(request, next_middleware) 13 | 14 | assert.spy(next_middleware).was_called() 15 | assert.contains(response, {status = 200, body = 'ok'}) 16 | end) 17 | end) 18 | -------------------------------------------------------------------------------- /middleware/accept-encoding-identity/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Accept-Encoding = Identity", 3 | "description": "Sets the `Accept-Encoding` header to `identity`. This deactivates gzipped responses. This middleware is inserted by default in all new services.", 4 | "files": ["accept-encoding-identity.lua"], 5 | "spec": ["accept-encoding-identity_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/add-args/add_args.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | request.args.new_param = '1' -- adds new query param 3 | request.args.old_param = nil -- removes one if it was passed 4 | 5 | return next_middleware() 6 | end 7 | -------------------------------------------------------------------------------- /middleware/add-args/add_args_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("Add args", function() 4 | it("adds a header", function() 5 | local add = spec.middleware('add-args/add_args.lua') 6 | local request = spec.request({ method = 'GET', uri = '/?old_param=1&foo=2'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, { 9 | method = 'GET', 10 | uri = '/', 11 | args = {foo = '2', new_param = '1'} 12 | }) 13 | return {status = 200, body = 'ok'} 14 | end) 15 | 16 | local response = add(request, next_middleware) 17 | 18 | assert.spy(next_middleware).was_called() 19 | assert.contains(response, {status = 200, body = 'ok'}) 20 | end) 21 | end) 22 | -------------------------------------------------------------------------------- /middleware/add-args/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-args", 3 | "description": "adds a paramter", 4 | "files": ["add_args.lua"], 5 | "spec": ["add_args_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/add-keys/add_keys.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | request.headers.authentication = 'this-is-my-key' 3 | return next_middleware() 4 | end 5 | -------------------------------------------------------------------------------- /middleware/add-keys/add_keys_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("Add keys", function() 4 | it("adds a header", function() 5 | local add = spec.middleware('add-keys/add_keys.lua') 6 | local request = spec.request({ method = 'GET', uri = '/'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, { 9 | method = 'GET', 10 | uri = '/', 11 | headers = { authentication = 'this-is-my-key'} 12 | }) 13 | return {status = 200, body = 'ok'} 14 | end) 15 | 16 | local response = add(request, next_middleware) 17 | 18 | assert.spy(next_middleware).was_called() 19 | assert.contains(response, {status = 200, body = 'ok'}) 20 | end) 21 | end) 22 | -------------------------------------------------------------------------------- /middleware/add-keys/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Add Keys", 3 | "description": "Add authentication key as a header.", 4 | "files": ["add_keys.lua"], 5 | "spec": ["add_keys_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/bicing-distance/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bicing Stations Distance", 3 | "description": "Calculates air distance to Barcelona Bicing station from given point and returns stations ordered by that distance.", 4 | "files": ["bicing_distance.lua"], 5 | "spec": ["bicing_distance_spec.lua"], 6 | "author": "mikz", 7 | "github_user" : "mikz", 8 | "email": "michal@3scale.net", 9 | "version": "1.0.0", 10 | "categories": [ 11 | "bicing" 12 | ], 13 | "endpoints": [ 14 | "http://wservice.viabicing.cat/" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /middleware/bicing-distance/bicing_distance.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware calculates distance from bicing stations to Pl. Catalunya. 4 | -- 5 | --]] 6 | 7 | -- Pl. Catalunya 8 | local lat = 41.3876780 9 | local long = 2.16958700 10 | 11 | local geo = { radius = 3958.75587 } 12 | 13 | function geo.distance(lat1, lng1, lat2, lng2) 14 | local rlat = lat1*math.pi/180; 15 | local rlng = lng1*math.pi/180; 16 | local rlat2 = lat2*math.pi/180; 17 | local rlng2 = lng2*math.pi/180; 18 | 19 | if (rlat == rlat2 and rlng == rlng2) then 20 | return 0 21 | else 22 | -- Spherical Law of Cosines 23 | return geo.radius*math.acos(math.sin(rlat)*math.sin(rlat2) 24 | +math.cos(rlng-rlng2)*math.cos(rlat)*math.cos(rlat2)) 25 | end 26 | end 27 | 28 | return function(request, next_middleware) 29 | local response = next_middleware() 30 | 31 | local stations = json.decode(response.body) 32 | 33 | for _,station in ipairs(stations) do 34 | station.distance = geo.distance(station.lat, station.long, lat, long) 35 | end 36 | 37 | table.sort(stations, function(one,two) return one.distance < two.distance end) 38 | 39 | response.body = json.encode(stations) 40 | 41 | return response 42 | end 43 | -------------------------------------------------------------------------------- /middleware/bicing-distance/bicing_distance_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("bicing_distance", function() 5 | 6 | it("adds a distance attribute to all the elements in the received json array, using their lat and long", function() 7 | 8 | local input_json = cjson.encode({ 9 | {name="barceloneta", lat=41.382335, long=2.185941}, 10 | {name="sagrada-familia", lat=41.403575, long=2.174483} 11 | }) 12 | 13 | local bicing_distance = spec.middleware('bicing-distance/bicing_distance.lua') 14 | local request = spec.request({method = 'GET', uri = '/'}) 15 | local next_middleware = spec.next_middleware(function() 16 | assert.contains(request, { method = 'GET', uri = '/' }) 17 | return {status = 200, body = input_json} 18 | end) 19 | 20 | local response = bicing_distance(request, next_middleware) 21 | 22 | assert.spy(next_middleware).was_called() 23 | 24 | assert.contains(response, {status = 200 }) 25 | 26 | local response_info = cjson.decode(response.body) 27 | assert.contains(response_info, { 28 | {name="barceloneta", lat=41.382335, long=2.185941}, 29 | {name="sagrada-familia", lat=41.403575, long=2.174483} 30 | }) 31 | 32 | -- Floating point comparisons can vary depending on the LuaJIT version; 33 | -- compare them using a threshold 34 | assert.difference(response_info[1].distance, 0.92467453001395) 35 | assert.difference(response_info[2].distance, 1.1273100485806) 36 | end) 37 | 38 | end) 39 | 40 | 41 | -------------------------------------------------------------------------------- /middleware/bicing-json/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bicing XML to JSON", 3 | "description": "Barcelona's Bicing API returns XML. This middleware converts it to slick JSON.", 4 | "files": ["bicing_json.lua"], 5 | "spec": ["bicing_json_spec.lua"], 6 | "author": "mikz", 7 | "github_user" : "mikz", 8 | "email": "michal@3scale.net", 9 | "version": "1.0.0", 10 | "categories": [ 11 | "bicing", 12 | "xml", 13 | "json" 14 | ], 15 | "endpoints": [ 16 | "http://wservice.viabicing.cat/" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /middleware/bicing-json/bicing_json.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware converts Bicing XML to JSON. 4 | -- 5 | --]] 6 | 7 | local function parse_xml(xml, xml_string) 8 | local stations = {} 9 | local station 10 | local attribute 11 | 12 | local parser = xml.new({ 13 | StartElement = function(parser, tag, attrs) 14 | if tag == 'station' then 15 | station = {} 16 | elseif station then 17 | attribute = tag 18 | end 19 | end, 20 | CharacterData = function(parser, value) 21 | if station and attribute then 22 | station[attribute] = value 23 | attribute = nil 24 | end 25 | end, 26 | EndElement = function(parser, tag) 27 | if tag == 'station' then 28 | stations[#stations + 1] = station 29 | station = nil 30 | end 31 | attribute = nil 32 | end 33 | }) 34 | parser:parse(xml_string) 35 | 36 | return stations 37 | end 38 | 39 | return function(request, next_middleware) 40 | local response = next_middleware() 41 | response.body = json.encode(parse_xml(xml, response.body)) 42 | return response 43 | end 44 | -------------------------------------------------------------------------------- /middleware/bicing-json/bicing_json_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("bicing_json", function() 4 | 5 | it("transforms stations xml into json", function() 6 | local xml = ([[ 7 | 8 | peter2 9 | jane3 10 | ann10 11 | 12 | ]]):gsub('%s', '') 13 | 14 | local json = ([[ 15 | [{"name":"peter","distance":"2"},{"name":"jane","distance":"3"},{"name":"ann","distance":"10"}] 16 | ]]):gsub('%s', '') 17 | 18 | 19 | local bicing_json = spec.middleware('bicing-json/bicing_json.lua') 20 | local request = spec.request({method = 'GET', uri = '/'}) 21 | local next_middleware = spec.next_middleware(function() 22 | assert.contains(request, { method = 'GET', uri = '/' }) 23 | return {status = 200, body = xml} 24 | end) 25 | 26 | local response = bicing_json(request, next_middleware) 27 | 28 | assert.spy(next_middleware).was_called() 29 | assert.contains(response, {status = 200, body = json}) 30 | end) 31 | 32 | end) 33 | 34 | 35 | -------------------------------------------------------------------------------- /middleware/bicing-walking-distance/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bicing Walking Distance", 3 | "description": "Gets walking distance to Barcelona Bicing stations from given point. Uses Google Maps.", 4 | "files": ["bicing_walking_distance.lua"], 5 | "spec": ["bicing_walking_distance_spec.lua"], 6 | "author": "mikz", 7 | "github_user" : "mikz", 8 | "email": "michal@3scale.net", 9 | "version": "1.0.0", 10 | "categories": [ 11 | "bicing" 12 | ], 13 | "endpoints": [ 14 | "http://wservice.viabicing.cat/" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /middleware/bicing-walking-distance/bicing_walking_distance.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware takes 10 stations and gets walking distance from Google Maps. 4 | -- 5 | --]] 6 | 7 | -- Pl. Catalunya 8 | local lat = 41.3876780 9 | local long = 2.16958700 10 | 11 | local maps = 'https://maps.googleapis.com//maps/api/distancematrix/json' 12 | 13 | return function(request, next_middleware) 14 | local response = next_middleware() 15 | 16 | local stations = json.decode(response.body) 17 | local nearest = {} 18 | for i=1,10 do 19 | nearest[i] = stations[i] 20 | end 21 | 22 | local requests = {} 23 | for i,station in ipairs(nearest) do 24 | requests[i] = { 25 | method = 'GET', 26 | url = maps .. "?origins="..lat..','..long.."&destinations="..station.lat ..","..station.long.."&mode=walking&sensor=false" 27 | } 28 | end 29 | 30 | local responses = http.multi(requests) 31 | 32 | for i,resp in ipairs(responses) do 33 | nearest[i].route = json.decode(resp.body) 34 | end 35 | 36 | response.body = json.encode(nearest) 37 | 38 | return response 39 | end 40 | -------------------------------------------------------------------------------- /middleware/bicing-walking-distance/bicing_walking_distance_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("bicing_walking_distance", function() 5 | 6 | it("adds a walking distance route to all the stations on the given json, asking Google", function() 7 | 8 | local input_json = ([[ 9 | [ 10 | {"name":"barceloneta", "lat":41.382335, "long":2.185941}, 11 | {"name":"sagrada-familia", "lat":41.403575, "long":2.174483} 12 | ] 13 | ]]):gsub('%s', '') 14 | 15 | local bicing_walking_distance = spec.middleware('bicing-walking-distance/bicing_walking_distance.lua') 16 | local request = spec.request({method = 'GET', uri = '/'}) 17 | local next_middleware = spec.next_middleware(function() 18 | assert.contains(request, { method = 'GET', uri = '/' }) 19 | return {status = 200, body = input_json} 20 | end) 21 | 22 | local gapi_start = 'https://maps.googleapis.com//maps/api/distancematrix/json?mode=walking&sensor=false&origins=41.387678,2.169587' 23 | spec.mock_http({url = gapi_start .. '&destinations=41.382335,2.185941'},{body = '[1,2,3]'}) 24 | spec.mock_http({url = gapi_start .. '&destinations=41.403575,2.174483'},{body = '[4,5,6]'}) 25 | 26 | local response = bicing_walking_distance(request, next_middleware) 27 | 28 | assert.spy(next_middleware).was_called() 29 | 30 | assert.contains(response, {status = 200 }) 31 | 32 | assert.same(cjson.decode(response.body), { 33 | {name="barceloneta", lat=41.382335, long=2.185941, route = {1,2,3}}, 34 | {name="sagrada-familia", lat=41.403575, long=2.174483, route = {4,5,6}} 35 | }) 36 | end) 37 | 38 | end) 39 | 40 | 41 | -------------------------------------------------------------------------------- /middleware/burningman-categorize/README.md: -------------------------------------------------------------------------------- 1 | # BurningMan Categorize middleware 2 | 3 | ## How to use it 4 | 5 | 1. Define Burningman API endpoint as you APItools service URL `http://playaevents.burningman.com/api/0.2/{year}/` 6 | 2. Add more categories depending on criterias 7 | 3. Make call to http://YOUROWN.apitools.com/event 8 | 9 | ## Existing categories (BM 2014) 10 | 11 | * *cere* for Ceremonies 12 | * *prty* for Gathering / Party 13 | * *work* for Class / Workshop 14 | * *care* for Care / Support 15 | * *kid* for Kid-friendly 16 | * *adlt* for Adult-oriented 17 | * *game* for Game 18 | * *para* for Parades 19 | * *fire* for Fire 20 | * *food* for Food 21 | * *perf* for Performance 22 | * *none* for Uncategorized -------------------------------------------------------------------------------- /middleware/burningman-categorize/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Burningman categorize events", 3 | "description": "Categorize events by type", 4 | "files": ["burningman_categorize.lua"], 5 | "spec": ["burningman_categorize_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["http://playaevents.burningman.com/api/0.2/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/burningman-categorize/burningman_categorize.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | -- every middleware has to call next_middleware, 3 | -- so others have chance to process the request/response 4 | 5 | local response = next_middleware() 6 | local events = json.decode(response.body) 7 | local parties = {} --prty 8 | local workshops ={} -- work 9 | local nocategory={} --none 10 | local ceremonies = {} --cere 11 | local cares ={} -- care 12 | local kids = {} -- kid 13 | local adults = {} --adlt 14 | local games ={} --game 15 | local parades ={} --para 16 | local fire={} --fire 17 | local food ={} --food 18 | local performances ={} --perf 19 | 20 | 21 | for i=1,#events do 22 | local currentEvent = events[i] 23 | 24 | if(currentEvent.category=='cere') then 25 | table.insert(ceremonies,currentEvent) 26 | elseif (currentEvent.category=='prty') then 27 | table.insert(parties,currentEvent) 28 | elseif(currentEvent.category=='work') then 29 | table.insert(workshops,currentEvent) 30 | elseif(currentEvent.category=='none') then 31 | table.insert(nocategory,currentEvent) 32 | elseif(currentEvent.category=='care') then 33 | table.insert(cares,currentEvent) 34 | elseif(currentEvent.category=='kid') then 35 | table.insert(kids,currentEvent) 36 | elseif(currentEvent.category=='adlt') then 37 | table.insert(adults,currentEvent) 38 | elseif(currentEvent.category=='game') then 39 | table.insert(games,currentEvent) 40 | elseif(currentEvent.category=='para') then 41 | table.insert(parades,currentEvent) 42 | elseif(currentEvent.category=='fire') then 43 | table.insert(fire,currentEvent) 44 | elseif(currentEvent.category=='food') then 45 | table.insert(food,currentEvent) 46 | elseif(currentEvent.category=='perf') then 47 | table.insert(performances,currentEvent) 48 | end 49 | end 50 | 51 | local datainfo ={} 52 | datainfo.nb_events = #events 53 | datainfo.nb_parties = #parties 54 | datainfo.nb_workshops = #workshops 55 | datainfo.nb_ceremonies = #ceremonies 56 | datainfo.nb_cares = #cares 57 | datainfo.nb_adults = #adults 58 | datainfo.nb_games = #games 59 | datainfo.nb_food = #food 60 | datainfo.nb_fire = #fire 61 | datainfo.nb_performances = #performances 62 | datainfo.nb_parades = #parades 63 | datainfo.nb_kids = #kids 64 | datainfo.nb_uncategorized = #nocategory 65 | 66 | local info = {} 67 | info.parties = parties 68 | info.workshops = workshops 69 | info.ceremonies = ceremonies 70 | info.cares = cares 71 | info.adults = adults 72 | info.games = games 73 | info.food = food 74 | info.fire = fire 75 | info.performances = performances 76 | info.parades = parades 77 | info.kids = kids 78 | info.uncategorized = nocategory 79 | info.datainfo = datainfo 80 | 81 | response.body = json.encode(info) 82 | 83 | return response 84 | end 85 | -------------------------------------------------------------------------------- /middleware/burningman-categorize/burningman_categorize_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("burningman_categorize", function() 5 | 6 | it("Adds a bunch of stuff to the response", function() 7 | 8 | local backend_json = cjson.encode({ 9 | {name='foo', category='prty'}, 10 | {name='bar', category='prty'}, 11 | {name='baz', category='perf'}, 12 | }) 13 | 14 | local burningman_categorize = spec.middleware('burningman-categorize/burningman_categorize.lua') 15 | local request = spec.request({method = 'GET', uri = '/'}) 16 | local next_middleware = spec.next_middleware(function() 17 | assert.contains(request, { method = 'GET', uri = '/' }) 18 | return {status = 200, body = backend_json} 19 | end) 20 | 21 | local response = burningman_categorize(request, next_middleware) 22 | 23 | assert.spy(next_middleware).was_called() 24 | 25 | assert.contains(response, {status = 200 }) 26 | 27 | local info = cjson.decode(response.body) 28 | 29 | assert.same(info.parties, {{name='foo', category='prty'}, {name='bar', category='prty'}}) 30 | assert.same(info.performances, {{name='baz', category='perf'}}) 31 | assert.same(info.fire, {}) 32 | 33 | assert.equal(info.datainfo.nb_parties, 2) 34 | assert.equal(info.datainfo.nb_performances, 1) 35 | assert.equal(info.datainfo.nb_fire, 0) 36 | end) 37 | end) 38 | 39 | 40 | -------------------------------------------------------------------------------- /middleware/burningman-demultiply/README.md: -------------------------------------------------------------------------------- 1 | # BurningMan Demultiply middleware 2 | 3 | # Why 4 | 5 | The original endpoint returns event with an *occurrence_set* array containing all the dates the event is happening. 6 | 7 | This middleware creates single event for each occurent of an event. 8 | 9 | ## How to use it 10 | 11 | 1. Define Burningman API endpoint as you APItools service URL `http://playaevents.burningman.com/api/0.2/{year}/` 12 | 2. Make call to http://YOUROWN.apitools.com/event -------------------------------------------------------------------------------- /middleware/burningman-demultiply/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Burningman demultiply events", 3 | "description": "If an event happens more than once, create a new event.", 4 | "files": ["burningman_demultiply.lua"], 5 | "spec": ["burningman_demultiply_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["http://playaevents.burningman.com/api/0.2/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/burningman-demultiply/burningman_demultiply.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | 3 | local response = next_middleware() 4 | local events = json.decode(response.body) 5 | local newresponse ={} 6 | 7 | for i=1,#events do 8 | local currentEvent = events[i]; 9 | 10 | for j=1,#currentEvent.occurrence_set do 11 | table.insert(newresponse,{ 12 | title = currentEvent.title, 13 | desc = currentEvent.description, 14 | id = currentEvent.id, 15 | host = currentEvent.hosted_by_camp, 16 | url = currentEvent.url, 17 | location = currentEvent.other_location, 18 | category = currentEvent.event_type.abbr, 19 | start_time = currentEvent.occurrence_set[j].start_time, 20 | end_time = currentEvent.occurrence_set[j].end_time 21 | }) 22 | end 23 | end 24 | 25 | response.body = json.encode(newresponse) 26 | 27 | return response 28 | end 29 | -------------------------------------------------------------------------------- /middleware/burningman-demultiply/burningman_demultiply_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("burningman_demultiply", function() 5 | 6 | it("Planarizes the events using start_time and end_time", function() 7 | 8 | local backend_json = cjson.encode({ 9 | { 10 | title='event1',description='desc1',id=1,hosted_by_camp='peter',url='url1',other_location='loc1', 11 | event_type = { abbr ='prty' }, 12 | occurrence_set = { 13 | {start_time = 1, end_time = 2}, 14 | {start_time = 2, end_time = 3}, 15 | {start_time = 3, end_time = 4}, 16 | } 17 | },{ 18 | title='event2',description='desc2',id=2,hosted_by_camp='john',url='url2',other_location='loc2', 19 | event_type = { abbr ='fire' }, 20 | occurrence_set = { 21 | {start_time = 10, end_time = 50}, 22 | } 23 | } 24 | }) 25 | 26 | local burningman_demultiply = spec.middleware('burningman-demultiply/burningman_demultiply.lua') 27 | local request = spec.request({method = 'GET', uri = '/'}) 28 | local next_middleware = spec.next_middleware(function() 29 | assert.contains(request, { method = 'GET', uri = '/' }) 30 | return {status = 200, body = backend_json} 31 | end) 32 | 33 | local response = burningman_demultiply(request, next_middleware) 34 | 35 | assert.spy(next_middleware).was_called() 36 | 37 | assert.contains(response, {status = 200}) 38 | 39 | local info = cjson.decode(response.body) 40 | 41 | assert.same(info, { 42 | { title='event1',desc='desc1',id=1,host='peter',url='url1',location='loc1',category='prty', start_time = 1, end_time = 2}, 43 | { title='event1',desc='desc1',id=1,host='peter',url='url1',location='loc1',category='prty', start_time = 2, end_time = 3}, 44 | { title='event1',desc='desc1',id=1,host='peter',url='url1',location='loc1',category='prty', start_time = 3, end_time = 4}, 45 | { title='event2',desc='desc2',id=2,host='john',url='url2',location='loc2',category='fire', start_time = 10, end_time = 50} 46 | }) 47 | end) 48 | 49 | end) 50 | 51 | 52 | -------------------------------------------------------------------------------- /middleware/cache-middleware/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cache", 3 | "description": "Caches responses so you don't have to care about rate limiting!", 4 | "files": ["cache.lua"], 5 | "spec": ["cache_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/cache-middleware/cache.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | -- initialize cache store 3 | 4 | local threshold = 60 -- 60 seconds 5 | local key = 'cache=' .. request.uri_full 6 | 7 | if request.method == "GET" then 8 | local stored = bucket.middleware.get(key) 9 | if stored then 10 | local expires = stored.headers['X-Expires'] 11 | 12 | if expires and expires > time.now() then -- not expired yet 13 | -- send.event({channel = "cache", msg = "returned cached content", level = "debug", key = key, content = stored, expires = expires, now = time.now() }) 14 | stored.headers['Expires'] = time.http(expires) 15 | end 16 | return stored 17 | end 18 | -- send.event({channel = "cache", msg = "NOT cached content", level = "debug", key = key, content = stored, expires = expires, now = time.now() }) 19 | end 20 | 21 | -- if content is not cached, do the real request & get response 22 | local response = next_middleware() 23 | 24 | if request.method == 'GET' then 25 | local expires = time.now() + threshold 26 | response.headers['X-Expires'] = expires 27 | bucket.middleware.set(key, response, expires) 28 | -- send.event({channel = "cache", msg = "stored cached content", level = "debug", content = response }) 29 | end 30 | 31 | return response 32 | end 33 | -------------------------------------------------------------------------------- /middleware/cache-middleware/cache_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("404 alert", function() 4 | local cache 5 | before_each(function() 6 | cache = spec.middleware('cache-middleware/cache.lua') 7 | end) 8 | 9 | describe("when the method is not GET", function() 10 | it("does nothing", function() 11 | local request = spec.request({method = 'POST', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'POST', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = cache(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | 21 | assert.contains(response, {status = 200, body = 'ok'}) 22 | 23 | assert.equal(#spec.bucket.middleware.get_keys(), 0) 24 | end) 25 | end) 26 | 27 | describe("when the method is GET", function() 28 | it("the first time, it just calls the next middleware, but sets some stuff on the middleware bucket", function() 29 | local request = spec.request({uri = '/'}) 30 | local next_middleware = spec.next_middleware(function() 31 | assert.contains(request, {method = 'GET', uri = '/'}) 32 | return {status = 200, body = 'ok'} 33 | end) 34 | 35 | local response = cache(request, next_middleware) 36 | 37 | assert.spy(next_middleware).was_called() 38 | 39 | assert.contains(response, {status = 200, body = 'ok', headers={}}) 40 | assert.equal(type(response.headers['X-Expires']), 'number') 41 | 42 | assert.equal(#spec.bucket.middleware.get_keys(), 1) 43 | local stored = spec.bucket.middleware.get('cache=http://localhost/') 44 | 45 | assert.contains(stored, {status = 200, body = 'ok', headers = {}}) 46 | assert.equal(type(stored.headers['X-Expires']), 'number') 47 | end) 48 | 49 | describe("when called twice", function() 50 | it("returns the cached result calling next_middleware only once", function() 51 | local request = spec.request({uri = '/'}) 52 | local next_middleware = spec.next_middleware(function() 53 | assert.contains(request, {method = 'GET', uri = '/'}) 54 | return {status = 200, body = 'ok'} 55 | end) 56 | 57 | local response1 = cache(request, next_middleware) 58 | local response2 = cache(request, next_middleware) 59 | 60 | assert.spy(next_middleware).was_called(1) 61 | 62 | assert.contains(response1, {status = 200, body = 'ok', headers={}}) 63 | assert.equal(type(response1.headers['X-Expires']), 'number') 64 | 65 | assert.contains(response2, {status = 200, body = 'ok', headers={}}) 66 | assert.equal(type(response1.headers['X-Expires']), 'number') 67 | 68 | assert.equal(#spec.bucket.middleware.get_keys(), 1) 69 | local stored = spec.bucket.middleware.get('cache=http://localhost/') 70 | 71 | assert.contains(stored, {status = 200, body = 'ok', headers = {}}) 72 | assert.equal(type(stored.headers['X-Expires']), 'number') 73 | end) 74 | 75 | it("forgets the cached result after a while, calling next_middleware twice", function() 76 | local request = spec.request({uri = '/'}) 77 | local next_middleware = spec.next_middleware(function() 78 | assert.contains(request, {method = 'GET', uri = '/'}) 79 | return {status = 200, body = 'ok'} 80 | end) 81 | 82 | local response1 = cache(request, next_middleware) 83 | 84 | spec.advance_time(61) 85 | 86 | local response2 = cache(request, next_middleware) 87 | 88 | assert.spy(next_middleware).was_called(2) 89 | 90 | assert.contains(response1, {status = 200, body = 'ok', headers={}}) 91 | assert.equal(type(response1.headers['X-Expires']), 'number') 92 | 93 | assert.contains(response2, {status = 200, body = 'ok', headers={}}) 94 | assert.equal(type(response1.headers['X-Expires']), 'number') 95 | 96 | assert.equal(#spec.bucket.middleware.get_keys(), 1) 97 | local stored = spec.bucket.middleware.get('cache=http://localhost/') 98 | 99 | assert.contains(stored, {status = 200, body = 'ok', headers = {}}) 100 | assert.equal(type(stored.headers['X-Expires']), 'number') 101 | end) 102 | end) 103 | end) 104 | end) 105 | -------------------------------------------------------------------------------- /middleware/citybikeAPI-distance/README.md: -------------------------------------------------------------------------------- 1 | # CityBike sort by location 2 | 3 | ## How to use it 4 | 5 | 1. Define Citybike API endpoint as you APItools service URL `http://www.bayareabikeshare.com/` (works as well with NYC Citybike service) 6 | 2. Change default lat, long values to the one wanted (could also be passed in the url and accesed using *req.params.lat* ). 7 | 3. Call `http://token.APITOOLS.com/stations/json` to retrieve data 8 | 9 | ## Tutorial 10 | More descriptive tutorial can be found [here](https://docs.apitools.com/blog/2014/06/19/apitools-demo-at-apidays-swift-lua-bike-share-api.html) -------------------------------------------------------------------------------- /middleware/citybikeAPI-distance/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citybike - sort by distance", 3 | "description": "Sort stations information CityBike API by location", 4 | "files": ["citybike_location.lua"], 5 | "author" : "Picsoung", 6 | "email" : "nicolas@3scale.net", 7 | "github_user" : "Picsoung", 8 | "version" : "1.0.0", 9 | "categories" : ["APIs"], 10 | "endpoints": ["http://www.citibikenyc.com/stations/json"] 11 | } 12 | -------------------------------------------------------------------------------- /middleware/citybikeAPI-distance/citybike_location.lua: -------------------------------------------------------------------------------- 1 | -- Center of San Francisco 2 | local lat = 37.7873589 3 | local long = -122.408227 4 | 5 | local geo = { radius = 3958.75587 } 6 | 7 | function geo.distance(lat1, lng1, lat2, lng2) 8 | local rlat = lat1*math.pi/180; 9 | local rlng = lng1*math.pi/180; 10 | local rlat2 = lat2*math.pi/180; 11 | local rlng2 = lng2*math.pi/180; 12 | 13 | if (rlat == rlat2 and rlng == rlng2) then 14 | return 0 15 | else 16 | -- Spherical Law of Cosines 17 | return geo.radius*math.acos(math.sin(rlat)*math.sin(rlat2) 18 | +math.cos(rlng-rlng2)*math.cos(rlat)*math.cos(rlat2)) 19 | end 20 | end 21 | 22 | return function(request, next_middleware) 23 | local response = next_middleware() 24 | 25 | local stations = json.decode(response.body).stationBeanList 26 | 27 | for _,station in ipairs(stations) do 28 | station.distance = geo.distance(station.latitude, station.longitude, lat, long) 29 | end 30 | 31 | table.sort(stations, function(one,two) return one.distance < two.distance end) 32 | 33 | response.body = json.encode(stations) 34 | 35 | return response 36 | end -------------------------------------------------------------------------------- /middleware/citybikeAPI-distance/citybike_location_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("citybike_distance", function() 5 | 6 | it("adds a distance attribute to all the elements in the received json array, using their lat and long. Also, sorts by distance", function() 7 | 8 | local input_json = cjson.encode({ 9 | stationBeanList = { 10 | {name="Hipster Men Clothes", latitude=37.761353, longitude = -122.4298161}, 11 | {name="Fancy Coffee Shop", latitude=37.760288, longitude = -122.504993 }, 12 | {name="Apple Store", latitude=37.785991, longitude = -122.406470 } 13 | } 14 | }) 15 | 16 | local citibike_distance = spec.middleware('citybikeAPI-distance/citybike_location.lua') 17 | local request = spec.request({method = 'GET', uri = '/'}) 18 | local next_middleware = spec.next_middleware(function() 19 | assert.contains(request, { method = 'GET', uri = '/' }) 20 | return {status = 200, body = input_json} 21 | end) 22 | 23 | local response = citibike_distance(request, next_middleware) 24 | 25 | assert.spy(next_middleware).was_called() 26 | 27 | assert.contains(response, {status = 200 }) 28 | 29 | local response_info = cjson.decode(response.body) 30 | assert.contains(response_info, { 31 | {name="Apple Store", latitude=37.785991, longitude = -122.406470 }, 32 | {name="Hipster Men Clothes", latitude=37.761353, longitude = -122.4298161}, 33 | {name="Fancy Coffee Shop", latitude=37.760288, longitude = -122.504993 } 34 | }) 35 | 36 | -- Floating point comparisons can vary depending on the LuaJIT version; 37 | -- compare them using a threshold 38 | assert.difference(response_info[1].distance, 0.13467403418529) 39 | assert.difference(response_info[2].distance, 2.1491348395457) 40 | assert.difference(response_info[3].distance, 5.605989319191) 41 | end) 42 | end) 43 | 44 | 45 | -------------------------------------------------------------------------------- /middleware/citybikeAPI/README.md: -------------------------------------------------------------------------------- 1 | # CityBike reduce payload 2 | 3 | ## How to use it 4 | 5 | 1. Define Citybike API endpoint as you APItools service URL `http://www.bayareabikeshare.com/` (works as well with NYC Citybike service) 6 | 2. Change number of results wanted (default 5). 7 | 3. Call `http://token.APITOOLS.com/stations/json` to retrieve data 8 | 9 | ## Tutorial 10 | More descriptive tutorial can be found [here](https://docs.apitools.com/blog/2014/06/19/apitools-demo-at-apidays-swift-lua-bike-share-api.html) -------------------------------------------------------------------------------- /middleware/citybikeAPI/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "citybike - reduce payload", 3 | "description": "Retrieve only relevant info from CityBike API", 4 | "files": ["citybike_payload.lua"], 5 | "spec": ["citybike_payload_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["http://www.citibikenyc.com/stations/json"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/citybikeAPI/citybike_payload.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | 3 | local response = next_middleware() 4 | local stations = json.decode(response.body) 5 | local nearest = {} 6 | -- change 5 to the number of results you are interested in. 7 | -- #stations for all of the stations 8 | 9 | for i=1,5 do 10 | nearest[i] = { 11 | stationName = stations[i].stationName, 12 | longitude = stations[i].longitude, 13 | latitude = stations[i].latitude, 14 | availableBikes = stations[i].availableBikes, 15 | availableDocks = stations[i].availableDocks, 16 | distance = stations[i].distance, 17 | city = stations[i].city 18 | } 19 | end 20 | 21 | response.body = json.encode(nearest) 22 | 23 | return response 24 | end 25 | -------------------------------------------------------------------------------- /middleware/citybikeAPI/citybike_payload_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("citybike_distance", function() 5 | 6 | it("adds a distance attribute to all the elements in the received json array, using their lat and long. Also, sorts by distance", function() 7 | 8 | local input_json = cjson.encode({ 9 | {stationName="Hipster Men Clothes", latitude=37.761353, longitude = -122.4298161, availableBikes=1, availableDocks=0, distance=1, city='sf', foo='bar'}, 10 | {stationName="Fancy Coffee Shop", latitude=37.760288, longitude = -122.504993, availableBikes=1, availableDocks=0, distance=2, city='sf', foo='bar' }, 11 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=3, city='sf', foo='bar' }, 12 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=4, city='sf', foo='bar' }, 13 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=5, city='sf', foo='bar' }, 14 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=6, city='sf', foo='bar' }, 15 | }) 16 | 17 | local citibike_payload = spec.middleware('citybikeAPI/citybike_payload.lua') 18 | local request = spec.request({method = 'GET', uri = '/'}) 19 | local next_middleware = spec.next_middleware(function() 20 | assert.contains(request, { method = 'GET', uri = '/' }) 21 | return {status = 200, body = input_json} 22 | end) 23 | 24 | local response = citibike_payload(request, next_middleware) 25 | 26 | assert.spy(next_middleware).was_called() 27 | 28 | assert.contains(response, {status = 200 }) 29 | 30 | assert.same(cjson.decode(response.body), { 31 | {stationName="Hipster Men Clothes", latitude=37.761353, longitude = -122.4298161, availableBikes=1, availableDocks=0, distance=1, city='sf'}, 32 | {stationName="Fancy Coffee Shop", latitude=37.760288, longitude = -122.504993, availableBikes=1, availableDocks=0, distance=2, city='sf' }, 33 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=3, city='sf' }, 34 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=4, city='sf' }, 35 | {stationName="Apple Store", latitude=37.785991, longitude = -122.406470, availableBikes=1, availableDocks=0, distance=5, city='sf' } 36 | }) 37 | end) 38 | end) 39 | 40 | 41 | -------------------------------------------------------------------------------- /middleware/cors/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CORS", 3 | "description": "Cross Origin Resource Sharing allows you to use API directly from the browser", 4 | "files": ["cors.lua"], 5 | "spec": ["cors_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc", "frontend"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/cors/cors.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | local res = next_middleware() 3 | -- Allow origin from certain domains (change as required) 4 | res.headers['Access-Control-Allow-Origin'] = "http://domain1.com http://domain2.com" 5 | -- Anable all domains (uncomment and comment the previous one if required) 6 | -- res.headers['Access-Control-Allow-Origin'] = "*" 7 | return res 8 | end 9 | -------------------------------------------------------------------------------- /middleware/cors/cors_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("CORS", function() 4 | it("adds a header", function() 5 | local cors = spec.middleware('cors/cors.lua') 6 | local request = spec.request({ method = 'GET', uri = '/'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, { method = 'GET', uri = '/'}) 9 | return {status = 200, body = 'ok'} 10 | end) 11 | 12 | local response = cors(request, next_middleware) 13 | 14 | assert.spy(next_middleware).was_called() 15 | assert.contains(response, {status = 200, body = 'ok', headers = {['Access-Control-Allow-Origin'] = "http://domain1.com http://domain2.com"}}) 16 | end) 17 | end) 18 | -------------------------------------------------------------------------------- /middleware/github-webhook-keen/README.md: -------------------------------------------------------------------------------- 1 | Send all the activity in your Github repo to Keen IO 2 | ---- 3 | 4 | 5 | 6 | Overview 7 | ---- 8 | Keen IO allows you to gather data from different places and consume it in a very easy way using their APIs. This middleware modifies the data provided from Github and send it to Keen IO. 9 | 10 | 11 | 12 | Requirements 13 | ---- 14 | * A [Keen IO](https://keen.io/) account 15 | * An [APItools](https://www.apitools.com/) account 16 | 17 | 18 | 19 | How to use it 20 | ---- 21 | 22 | 1. Set up a new monitor and point it to Keen IO (`https://api.keen.io/v3/`) 23 | 2. Go to the 'Pipeline' tab in your APITools monitor and add enable this middleware 24 | 3. Edit the middleware and substitute `YOUR-WRITE-KEEN-IO-TOKEN-HERE` with your write token from Keen IO 25 | 4. Go to your repo on Github and click on Settings >> Webhooks & Services >> Add webhook 26 | ![Add webhook screenshot](http://i.imgur.com/36JDgE9.png) 27 | 5. Configure the webhook 28 | * Add your APItools URL followed by the Keen IO project that you want to use. E.g `https://APItools_MONITOR_URL.my.apitools.com/projects/KEEN_IO_PROJECT_ID` **NOTE** Notice that you don't have to put v3 here but in the monitor 29 | * Select the events that you want to send. E.g `Send me everything.` 30 | 7. If everything went well, you should see all the new events from your repo in your project on Keen IO 31 | * The first event after you set up your webhook is a `ping` event -------------------------------------------------------------------------------- /middleware/github-webhook-keen/apitools.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Github webhooks to Keen IO", 4 | "description": "Send all the activity in your Github repo to Keen IO", 5 | "files": ["github-webhook-keen-io.lua"], 6 | "author" : "plunchete", 7 | "email" : "plunchete@gmail.com", 8 | "github_user" : "plunchete", 9 | "version" : "1.0.0", 10 | "categories" : ["data", "keen_IO", "github"], 11 | "endpoints": ["https://api.keen.io/3.0/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/github-webhook-keen/github-webhook-keen-io.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | 3 | local keen_write_token = "YOUR-WRITE-KEEN-IO-TOKEN-HERE" 4 | 5 | -- We want to modify the url by: 6 | -- #1 Adding the header X-GitHub-Event as name of the event 7 | -- #2 Add api_key 8 | local uri = request.uri 9 | 10 | -- if the url doesn't end with / add it 11 | if string.sub(uri,-string.len("/")) ~= "/" then 12 | uri = uri .. "/" 13 | end 14 | 15 | -- adding event to the url 16 | uri = uri .. "events/" .. request.headers["X-GitHub-Event"] 17 | 18 | -- adding the write token 19 | uri = uri .. '?api_key=' .. keen_write_token 20 | 21 | -- swap urls 22 | request.uri = uri 23 | 24 | local response = next_middleware() 25 | return response 26 | end -------------------------------------------------------------------------------- /middleware/http-cache/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Cache Middleware 2 | 3 | Drop the middleware to the Monitor and start using it by making HTTP calls. 4 | If the service responds with HTTP header `Cache-Control: public` and `Last-Modified` and `Etag` the response will be cached. 5 | Then if another call is made to the same URL and `max-age` set by `Cache-Control` did not expired yet the stored response will be used straight without rechecking. 6 | However, if `max-age` expired, the call will be made with `If-None-Match` and `If-Modified-Since` headers. If the response is 304, will send cached response to the client. 7 | 8 | ## Requirements 9 | 10 | * HTTP API sending correct caching headers 11 | 12 | ## TODO 13 | 14 | * flushing the cache 15 | * cache busting when the resource changes (does not return 304 during recheck) 16 | 17 | ## Development 18 | 19 | * api.github.com is good public API with caching 20 | 21 | -------------------------------------------------------------------------------- /middleware/http-cache/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTTP Cache", 3 | "description": "Cache server responses using HTTP semantis like Etags and Last-Modified.", 4 | "files": [ 5 | "http_cache.lua" 6 | ], 7 | "spec": [ 8 | "http_cache_spec.lua" 9 | ], 10 | "author": "Ludek Berecka", 11 | "email": "berecka.ludek@gmail.com", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "cache", 15 | "http" 16 | ], 17 | "endpoints": [ 18 | "*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /middleware/http-cache/http_cache.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware is using HTTP Semantics like Last-Modified and Etag to cache responses. 4 | -- Works transparently with clients that do not use caching. 5 | -- Naively implements Cache-Control private, no-cache and no-store (by not caching at all). 6 | -- Adds Age header to cached responses that indicates how long since last recheck. 7 | --]] 8 | 9 | local function use_cache(request) 10 | local cache_control = request.cache_control 11 | if cache_control['no-cache'] or cache_control['private'] or cache_control['no-store'] then 12 | return false 13 | end 14 | return request.method == 'GET' 15 | end 16 | 17 | local function split_header(string) 18 | local split = {} 19 | if not string then return split end 20 | for k in string.gmatch(string, "([a-z-]+)") do 21 | split[#split] = k 22 | split[k] = true 23 | end 24 | for k, v in string.gmatch(string, "([%w-]+)=(%w+)") do 25 | split[k] = v 26 | end 27 | return split 28 | end 29 | 30 | local function fresh(cache, request) 31 | local max_age = request.cache_control['max-age'] 32 | if max_age then 33 | return (time.now() - cache.stored) <= tonumber(max_age) 34 | else 35 | return true 36 | end 37 | end 38 | 39 | local function fetch(cached) 40 | local response = bucket.middleware.get(cached.etag) 41 | if response then 42 | response.headers['Age'] = math.ceil(time.now() - cached.stored) 43 | metric.count('cache.hit') 44 | end 45 | return response 46 | end 47 | 48 | local function fetch_cache(request) 49 | local cached = bucket.middleware.get(request.uri_relative) 50 | 51 | if cached and cached.etag then 52 | if fresh(cached, request) then 53 | return fetch(cached) 54 | end 55 | end 56 | end 57 | 58 | local function cache_response(request, response, cached) 59 | local cache_control = response.cache_control 60 | local max_age = tonumber(cache_control['max-age'] or cache_control['s-maxage']) or 0 61 | 62 | if not cache_control['public'] and max_age > 0 then return response end 63 | 64 | local last_modified = response.headers['Last-Modified'] 65 | local etag = response.headers['Etag'] 66 | 67 | local metadata = { last_modified = last_modified, etag = etag, max_age = max_age, stored = time.now() } 68 | 69 | if cached and cached.response and response.status == 304 then 70 | metric.count('cache.refresh') 71 | response = cached.response 72 | cached = metadata 73 | response.headers['Age'] = math.ceil(time.now() - cached.stored) 74 | elseif etag and response.status == 200 then 75 | bucket.middleware.set(request.uri_full, metadata) 76 | bucket.middleware.add(etag, response) 77 | metric.count('cache.stored') 78 | end 79 | 80 | bucket.middleware.set(request.uri_relative, metadata, max_age) 81 | 82 | return response 83 | end 84 | 85 | local function fetch_upstream(request, next_middleware) 86 | local cached = bucket.middleware.get(request.uri_full) 87 | 88 | if cached then 89 | cached.response = bucket.middleware.get(cached.etag) 90 | 91 | if cached.response then 92 | request.headers['If-None-Match'] = cached.etag 93 | request.headers['If-Modified-Since'] = cached.last_modified 94 | end 95 | end 96 | 97 | local response = next_middleware() 98 | response.cache_control = split_header(response.headers['Cache-Control']) 99 | 100 | return cache_response(request, response, cached) 101 | end 102 | 103 | return function (request, next_middleware) 104 | request.cache_control = split_header(request.headers['Cache-Control']) 105 | 106 | if use_cache(request) then 107 | return fetch_cache(request) or fetch_upstream(request, next_middleware) 108 | else 109 | metric.count('cache.miss') 110 | return next_middleware() 111 | end 112 | end 113 | 114 | -------------------------------------------------------------------------------- /middleware/http-cache/http_cache_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('HTTP Cache', function() 4 | local http_cache 5 | before_each(function() 6 | http_cache = spec.middleware('http-cache/http_cache.lua') 7 | end) 8 | 9 | it('it calls and returns next middleware', function() 10 | local request = spec.request({method = 'GET', uri = '/'}) 11 | local next_middleware = spec.next_middleware(function() 12 | assert.contains(request, {method = 'GET', uri = '/'}) 13 | return {status = 200, body = 'ok'} 14 | end) 15 | 16 | local response = http_cache(request, next_middleware) 17 | http_cache(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called(2) 20 | 21 | assert.contains(response, {status = 200, body = 'ok'}) 22 | end) 23 | 24 | it('stores response when it has all required headers', function() 25 | local request = spec.request({method = 'GET', uri = '/path'}) 26 | local next_middleware = spec.next_middleware(function() 27 | return { 28 | status = 200, 29 | body = 'ok', 30 | headers = { 31 | ['Etag'] = '"etag"', 32 | ['Last-Modified'] = "Sun, 30 Nov 2014 12:28:11 GMT", 33 | ['Cache-Control'] = "public, max-age=60, s-maxage=60" 34 | } 35 | } 36 | end) 37 | http_cache(request, next_middleware) 38 | assert.equal(#spec.bucket.middleware.get_keys(), 3) 39 | 40 | assert.spy(next_middleware).was_called() 41 | 42 | assert(spec.bucket.middleware.get('/path')) 43 | assert(spec.bucket.middleware.get('"etag"')) 44 | assert(spec.bucket.middleware.get('http://localhost/path')) 45 | end) 46 | 47 | it('stores caches the response', function() 48 | local request = spec.request({method = 'GET', uri = '/path'}) 49 | local next_middleware = spec.next_middleware(function() 50 | return { 51 | status = 200, 52 | body = 'ok', 53 | headers = { 54 | ['Etag'] = '"etag"', 55 | ['Last-Modified'] = "Sun, 30 Nov 2014 12:28:11 GMT", 56 | ['Cache-Control'] = "public, max-age=60, s-maxage=60" 57 | } 58 | } 59 | end) 60 | http_cache(request, next_middleware) 61 | http_cache(request, next_middleware) 62 | 63 | assert.spy(next_middleware).was_called(1) 64 | end) 65 | 66 | end) 67 | -------------------------------------------------------------------------------- /middleware/ifg-filter-category/README.md: -------------------------------------------------------------------------------- 1 | # Ifeelgoods filter by category middleware 2 | 3 | When using [ifeelgoods](http://ifeelgoods.com) API, filter rewards by category. 4 | 5 | ## Usage 6 | 7 | 1. Change `category` in `ifg_filter_rewards_by_category.lua` with the name of the category you want to retrieve. examples: *Cinema*,*E-commerce*. 8 | -------------------------------------------------------------------------------- /middleware/ifg-filter-category/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ifeelgoods, filter reward by category", 3 | "description": "Retrieve reward of a specific category", 4 | "files": [ 5 | "ifg_filter_rewards_by_category.lua" 6 | ], 7 | "spec": [ 8 | "ifg_filter_rewards_by_category_spec.lua" 9 | ], 10 | "author": "Nicolas Grenié", 11 | "email": "nicolas@3scale.net", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "filter", 15 | "reward" 16 | ], 17 | "endpoints": [ 18 | "https://api.ifeelgoods.com", 19 | "https://api-sandbox.ifeelgoods.com", 20 | "https://oauth.ifeelgoods.com", 21 | "https://oauth-sandbox.ifeelgoods.com" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /middleware/ifg-filter-category/ifg_filter_rewards_by_category.lua: -------------------------------------------------------------------------------- 1 | local indexOf = function( t, object ) 2 | if "table" == type( t ) then 3 | for i = 1, #t do 4 | if object == t[i] then 5 | return i 6 | end 7 | end 8 | return -1 9 | else 10 | error("indexOf expects table for first argument, " .. type(t) .. " given") 11 | end 12 | end 13 | 14 | return function(request, next_middleware) 15 | local response = next_middleware() 16 | local data = json.decode(response.body).data 17 | 18 | -- CHANGE THIS TO THE CATEGORY YOU ARE INTERESTED IN 19 | local category='ecommerce' 20 | 21 | local r ={} 22 | for i=1,#data do 23 | if(indexOf(data[i].categories, category) ~= -1) then 24 | table.insert(r,data[i]) 25 | end 26 | end 27 | 28 | response.body = json.encode({data=r}) 29 | return response 30 | end 31 | -------------------------------------------------------------------------------- /middleware/ifg-filter-category/ifg_filter_rewards_by_category_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("I feel good filter price", function() 5 | it("filters a price between min and maximum", function() 6 | local add = spec.middleware('ifg-filter-category/ifg_filter_rewards_by_category.lua') 7 | local request = spec.request({ method = 'GET', uri = '/'}) 8 | local next_middleware = spec.next_middleware(function() 9 | assert.contains(request, { 10 | method = 'GET', 11 | uri = '/' 12 | }) 13 | return {status = 200, body = '{"data": [{"categories": ["foo"]}, {"categories": ["ecommerce", "foo"]}, {"categories": ["ecommerce"]}]}' } 14 | end) 15 | 16 | local response = add(request, next_middleware) 17 | 18 | assert.spy(next_middleware).was_called() 19 | assert.equal(response.status, 200) 20 | assert.same(cjson.decode(response.body), {data={{categories={"ecommerce", "foo"}}, {categories={"ecommerce"}}}}) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /middleware/ifg-filter-price/README.md: -------------------------------------------------------------------------------- 1 | # Ifeelgoods filter by price middleware 2 | 3 | When using [ifeelgoods](http://ifeelgoods.com) API, filter rewards by price. 4 | 5 | ## Usage 6 | 7 | 1. Change `max_price` and `min_price` in `ifg_filter_rewards_by_price.lua` with the values you want. 8 | -------------------------------------------------------------------------------- /middleware/ifg-filter-price/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ifeelgoods, filter rewards by price", 3 | "description": "Retrieve rewards in a specific price range", 4 | "files": [ 5 | "ifg_filter_rewards_by_price.lua" 6 | ], 7 | "spec": [ 8 | "ifg_filter_rewards_by_price_spec.lua" 9 | ], 10 | "author": "Nicolas Grenié", 11 | "email": "nicolas@3scale.net", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "filter", 15 | "reward" 16 | ], 17 | "endpoints": [ 18 | "https://api.ifeelgoods.com", 19 | "https://api-sandbox.ifeelgoods.com", 20 | "https://oauth.ifeelgoods.com", 21 | "https://oauth-sandbox.ifeelgoods.com" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /middleware/ifg-filter-price/ifg_filter_rewards_by_price.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | local response = next_middleware() 3 | 4 | local data = json.decode(response.body).data 5 | 6 | -- CHANGE THIS TO WHATEVER YOU WANT 7 | local max_price = 9 8 | local min_price = 2 9 | 10 | local r={} 11 | for i=1,#data do 12 | if(tonumber(data[i].face_value)>= min_price and tonumber(data[i].face_value) <= max_price) then 13 | table.insert(r,data[i]) 14 | end 15 | end 16 | 17 | response.body = json.encode({data=r}) 18 | 19 | return response 20 | end 21 | -------------------------------------------------------------------------------- /middleware/ifg-filter-price/ifg_filter_rewards_by_price_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("I feel good filter price", function() 5 | it("filters a price between min and maximum", function() 6 | local add = spec.middleware('ifg-filter-price/ifg_filter_rewards_by_price.lua') 7 | local request = spec.request({ method = 'GET', uri = '/'}) 8 | local next_middleware = spec.next_middleware(function() 9 | assert.contains(request, { 10 | method = 'GET', 11 | uri = '/', 12 | }) 13 | return {status = 200, body = '{"data": [{"face_value": 1}, {"face_value": 5}, {"face_value": 10}]}' } 14 | end) 15 | 16 | local response = add(request, next_middleware) 17 | 18 | assert.spy(next_middleware).was_called() 19 | assert.equal(response.status, 200) 20 | assert.same(cjson.decode(response.body), {data={{face_value=5}}}) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /middleware/measure-x-ratelimit-remaining/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Measure X-Ratelimit-Remaining", 3 | "description": "Allows measure custom metrics.", 4 | "files": ["measure_x_ratelimit_remaining.lua"], 5 | "spec": ["measure_x_ratelimit_remaining_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["ploting", "analytics", "metrics"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/measure-x-ratelimit-remaining/measure_x_ratelimit_remaining.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | local response = next_middleware() 3 | 4 | local remaining = response.headers['X-Ratelimit-Remaining'] 5 | local limit = response.headers['X-Ratelimit-Limit'] 6 | 7 | if limit and remaining then 8 | remaining = tonumber(remaining) 9 | limit = tonumber(limit) 10 | metric.set('ratelimit-used', limit - remaining) 11 | metric.set('ratelimit-remaining', remaining) 12 | end 13 | 14 | return response 15 | end 16 | 17 | -------------------------------------------------------------------------------- /middleware/measure-x-ratelimit-remaining/measure_x_ratelimit_remaining_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('measure_x_ratelimiting_remaining', function() 4 | local rate 5 | before_each(function() 6 | rate = spec.middleware('measure-x-ratelimit-remaining/measure_x_ratelimit_remaining.lua') 7 | end) 8 | 9 | describe('when a response does not have the required headers', function() 10 | it('does nothing', function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | return {status = 200, body = 'ok'} 15 | end) 16 | 17 | local response = rate(request, next_middleware) 18 | 19 | assert.spy(next_middleware).was_called() 20 | assert.contains(response, {status = 200, body = 'ok'}) 21 | 22 | assert.is_nil(spec.metric.sets['ratelimit-used']) 23 | assert.is_nil(spec.metric.sets['ratelimit-remaining']) 24 | end) 25 | end) 26 | 27 | describe('when a response has the required headers', function() 28 | it('emmits metrics', function() 29 | local request = spec.request({method = 'GET', uri = '/'}) 30 | local next_middleware = spec.next_middleware(function() 31 | assert.contains(request, {method = 'GET', uri = '/'}) 32 | return { 33 | status = 200, 34 | body = 'ok', 35 | headers = { 36 | ['X-Ratelimit-Remaining'] = 1, 37 | ['X-Ratelimit-Limit'] = 5 38 | } 39 | } 40 | end) 41 | 42 | local response = rate(request, next_middleware) 43 | 44 | assert.spy(next_middleware).was_called() 45 | assert.contains(response, {status = 200, body = 'ok'}) 46 | 47 | assert.equal(spec.metric.sets['ratelimit-used'], 4) 48 | assert.equal(spec.metric.sets['ratelimit-remaining'], 1) 49 | end) 50 | end) 51 | end) 52 | -------------------------------------------------------------------------------- /middleware/privacy/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Privacy", 3 | "description": "All-purpose middleware. Customize to adjust it to your app's privacy concerns", 4 | "files": ["privacy.lua"], 5 | "spec": ["privacy_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["privacy"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/privacy/privacy.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- This middleware shows how to anonymize lots of different parts of a request in APItools, for various privacy concerns. 3 | -- 4 | -- Since each app is different, we can't provide a one-size-fits-all solution for this; you will have to 5 | -- adapt the code to suit your needs. 6 | -- 7 | -- The middleware completely commented out by default, so it is safe to include it on any Service. You can then start making 8 | -- modifications until all the data you need anonymized is gone. 9 | -- ]] 10 | 11 | return function (req, next_middleware) 12 | --> Call the endpoint without masking anything first. Otherwise you will send anonymized requests to the endpoint 13 | local res = next_middleware() 14 | 15 | --> What follows is a list of optional steps - ways to anonymize data in the requests/responses 16 | --> 17 | --> * Pick as many steps as you want, uncommenting their lua code. 18 | --> * You can duplicate any step if you need to 19 | --> * Adapt them to your field names 20 | --> * Don't uncomment the lines that begin with --> ; those are explanatory comments, not commented-out code. 21 | --> You can either leave them commented out or remove the whole line. 22 | 23 | --> BEGIN OF OPTIONAL STEPS <-- 24 | 25 | --> REQUEST HEADERS 26 | --> remove a REQUEST HEADER 27 | -- req.headers["Authorization"] = nil 28 | 29 | --> anonymize a REQUEST header 30 | -- req.headers["Authorization"] = "*****" 31 | 32 | --> remove ALL REQUEST HEADERS 33 | -- req.headers = {} 34 | 35 | --> RESPONSE HEADERS 36 | --> Do the same as in the REQUEST HEADERS above, replacing `req` by `res` 37 | 38 | --> remove a REQUEST QUERY PARAMETER 39 | -- req.query = req.query:gsub("api_key=[^&]+&?","") 40 | -- req.args["api_key"] = nil 41 | 42 | --> anonymize a REQUEST QUERY PARAMETER 43 | -- req.query = req.query:gsub("api_key=[^&]+","api_key=xxxx") 44 | -- req.args["api_key"] = "xxxx" 45 | 46 | --> remove ALL REQUEST QUERY PARAMETERS 47 | -- req.query = "" 48 | -- req.args = {} 49 | 50 | --> Remove a FIELD FROM A JSON REQUEST BODY 51 | -- local body = json.decode(req.body) 52 | -- body["user_id"] = nil 53 | -- req.body = json.encode(body) 54 | 55 | --> Anonymize a FIELD FROM THE JSON REQUEST BODY 56 | -- local body = json.decode(req.body) 57 | -- body["user_id"] = "xxxxxxx" 58 | -- req.body = json.encode(body) 59 | 60 | --> remove the REQUEST BODY 61 | -- req.body = "" 62 | 63 | --> RESPONSE BODY 64 | -- Same as the request body. Just replace `req` by `res`. 65 | 66 | --> Anonymize a PARTIAL PATH of the REQUEST URI 67 | --> Example: /foo/secret/bar/baz => /foo/secret/xxx/baz 68 | -- req.uri = req.uri:gsub("/secret/[^/]+", "/secret/xxx") 69 | -- req.uri_full = req.uri_full:gsub("/secret/[^/]+", "/secret/xxx") 70 | -- req.uri_relative = req.uri_relative:gsub("/secret/[^/]+", "/secret/xxx") 71 | 72 | --> Remove the REQUEST URI 73 | -- req.uri = "" 74 | -- req.uri_full = "" 75 | -- req.uri_relative = "" 76 | 77 | --> END OF OPTIONAL STEPS <-- 78 | 79 | --> Always remember to return the response at the end 80 | return res 81 | end 82 | -------------------------------------------------------------------------------- /middleware/privacy/privacy_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('privacy', function() 4 | it("does nothing (everything is commented out", function() 5 | local privacy = spec.middleware('privacy/privacy.lua') 6 | 7 | local request = spec.request({method = 'GET', uri = '/'}) 8 | local next_middleware = spec.next_middleware(function() 9 | assert.contains(request, {method = 'GET', uri = '/'}) 10 | return {status = 200, body = 'ok'} 11 | end) 12 | 13 | local response = privacy(request, next_middleware) 14 | 15 | assert.spy(next_middleware).was_called() 16 | assert.contains(response, {status = 200, body = 'ok'}) 17 | end) 18 | end) 19 | -------------------------------------------------------------------------------- /middleware/response-size-metric/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "response-size-metric", 3 | "description": "Adds a metric called 'size', with the response size. This metric can be used in the analytics tab to show graphics.", 4 | "files": ["response-size-metric.lua"], 5 | "spec": ["response-size-metric_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["analytics"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/response-size-metric/response-size-metric.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware adds a metric called "size", containing the request size. 4 | -- This metric can be latter used on the Analytics tab, to plot more info. 5 | -- 6 | -- You need to make at least one request using this middleware before it is possible to make the graph. 7 | -- Once the request is done, the graph in the analytics tab can be built like this: 8 | -- 9 | -- * Title: Response Size 10 | -- * Time Range: 30 min 11 | -- * Granularity: 1min 12 | -- * Metrics: Size (It won't appear until at least one request has been made using the new middleware) 13 | -- * Activate the "avg" flag, just below "size" 14 | -- * Methods: Deactivate all, but activate "One line per method" 15 | -- * Paths: Activate 1 line per path 16 | -- 17 | -- Now there should be a graph that displays the response size of each endpoint. 18 | -- The response will be averaged by granularity; this means that if the send endpoint returns several 19 | -- responses of different sizes, their averabe per minute/hour/day will be shown. 20 | -- 21 | --]] 22 | 23 | return function(request, next_middleware) 24 | local response = next_middleware() 25 | metric.set('size', #response.body) 26 | return response 27 | end 28 | -------------------------------------------------------------------------------- /middleware/response-size-metric/response-size-metric_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('response_size_metric', function() 4 | it('adds a metric with the size of the response body, in bytes', function() 5 | local response_size_metric = spec.middleware('response-size-metric/response-size-metric.lua') 6 | local request = spec.request({method = 'GET', uri = '/'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, {method = 'GET', uri = '/'}) 9 | return {status = 200, body = 'abcdef'} 10 | end) 11 | 12 | local response = response_size_metric(request, next_middleware) 13 | 14 | assert.spy(next_middleware).was_called() 15 | assert.contains(response, {status = 200, body = 'abcdef'}) 16 | 17 | assert.equal(6, spec.metric.sets['size']) 18 | end) 19 | end) 20 | -------------------------------------------------------------------------------- /middleware/response-time-alert/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Response Time Alert", 3 | "description": "Sends an email in case a call takes more than expected", 4 | "files": ["response_time_alert.lua"], 5 | "spec": ["response_time_alert_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["devops", "notifications"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/response-time-alert/response_time_alert.lua: -------------------------------------------------------------------------------- 1 | local five_mins = 60 * 5 2 | local threshold = 1.0 -- 1 second, for less use decimal numbers 3 | return function (request, next_middleware) 4 | local res = next_middleware() 5 | local last_mail = bucket.middleware.get('last_mail') 6 | 7 | if trace.time > threshold and (not last_mail or last_mail < time.now() - five_mins) then 8 | send.mail('YOUR-MAIL-HERE@gmail.com', 9 | "Trace took more than " .. tostring(threshold), 10 | request.uri_full .. " took more than " .. tostring(threshold) .. " to load. See full trace:" .. trace.link) 11 | bucket.middleware.set('last_mail', time.now()) 12 | end 13 | return res 14 | end 15 | 16 | -------------------------------------------------------------------------------- /middleware/response-time-alert/response_time_alert_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('response_time_alert', function() 4 | local response_time_alert 5 | before_each(function() 6 | response_time_alert = spec.middleware('response-time-alert/response_time_alert.lua') 7 | end) 8 | 9 | describe("when the response is given quickly", function() 10 | it("does nothing", function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | spec.advance_time(0.2) -- less than 1 second 15 | return {status = 200, body = 'ok'} 16 | end) 17 | 18 | local response = response_time_alert(request, next_middleware) 19 | 20 | assert.spy(next_middleware).was_called() 21 | assert.contains(response, {status = 200, body = 'ok'}) 22 | assert.equal(#spec.sent.emails, 0) 23 | assert.equal(#spec.bucket.middleware:get_keys(), 0) 24 | end) 25 | end) 26 | 27 | describe("when the response takes more than the threshold", function() 28 | it("sends an email the first time this happens", function() 29 | local request = spec.request({method = 'GET', uri = '/'}) 30 | local next_middleware = spec.next_middleware(function() 31 | assert.contains(request, {method = 'GET', uri = '/'}) 32 | spec.advance_time(2) -- 2 seconds 33 | return {status = 200, body = 'ok'} 34 | end) 35 | 36 | local response = response_time_alert(request, next_middleware) 37 | 38 | assert.spy(next_middleware).was_called() 39 | assert.contains(response, {status = 200, body = 'ok'}) 40 | assert.equal(#spec.sent.emails, 1) 41 | 42 | local last_email = spec.sent.emails.last 43 | assert.equal('YOUR-MAIL-HERE@gmail.com', last_email.to) 44 | assert.equal('Trace took more than 1', last_email.subject) 45 | assert.equal('http://localhost/ took more than 1 to load. See full trace:', last_email.message) 46 | 47 | assert.equal(type(spec.bucket.middleware.get('last_mail')), 'number' ) 48 | end) 49 | 50 | it("does not send two emails if two slow responses happen quickly after one another", function() 51 | local request = spec.request({method = 'GET', uri = '/'}) 52 | local next_middleware = spec.next_middleware(function() 53 | assert.contains(request, {method = 'GET', uri = '/'}) 54 | spec.advance_time(2) -- 2 seconds 55 | return {status = 200, body = 'ok'} 56 | end) 57 | 58 | local response1 = response_time_alert(request, next_middleware) 59 | spec.advance_time(2) -- 2 seconds 60 | local response2 = response_time_alert(request, next_middleware) 61 | 62 | assert.spy(next_middleware).was_called(2) 63 | assert.contains(response1, {status = 200, body = 'ok'}) 64 | assert.contains(response2, {status = 200, body = 'ok'}) 65 | assert.equal(#spec.sent.emails, 1) 66 | 67 | local last_email = spec.sent.emails.last 68 | assert.equal('YOUR-MAIL-HERE@gmail.com', last_email.to) 69 | assert.equal('Trace took more than 1', last_email.subject) 70 | assert.equal('http://localhost/ took more than 1 to load. See full trace:', last_email.message) 71 | 72 | assert.equal(type(spec.bucket.middleware.get('last_mail')), 'number' ) 73 | end) 74 | 75 | it("sends two emails if two slow responses happen with enough time separation between them", function() 76 | local request = spec.request({method = 'GET', uri = '/'}) 77 | local next_middleware = spec.next_middleware(function() 78 | assert.contains(request, {method = 'GET', uri = '/'}) 79 | spec.advance_time(2) -- 2 seconds 80 | return {status = 200, body = 'ok'} 81 | end) 82 | 83 | local response1 = response_time_alert(request, next_middleware) 84 | spec.advance_time(60*5 + 1) -- 5 minutes + 1 second 85 | local response2 = response_time_alert(request, next_middleware) 86 | 87 | assert.spy(next_middleware).was_called(2) 88 | assert.contains(response1, {status = 200, body = 'ok'}) 89 | assert.contains(response2, {status = 200, body = 'ok'}) 90 | 91 | assert.equal(#spec.sent.emails, 2) 92 | end) 93 | end) 94 | 95 | end) 96 | -------------------------------------------------------------------------------- /middleware/send-res-size-keen/README.md: -------------------------------------------------------------------------------- 1 | # Send of HTTP(s) responses' size to Keen IO 2 | 3 | ## Overview 4 | 5 | Keen IO allows you to gather data from different places and consume it in a very easy way using their APIs. This middleware send data from APItools - in this case the sizes of the HTTP(s) responses - to Keen IO. Any metric could be sent just changing the #response.body parameter. 6 | 7 | ## How to use it 8 | 9 | 1. Set up any API service that you want to monitor, e.g. `https://api.500px.com/v1/` 10 | 2. Make sure you're hitting the API and the requests are being logged on APItools. To achieve that check the 'Integration' tab, make a call (from your app or using cURL), and then go back to the 'Traces' tab to verify it went through. 11 | 3. Go to the 'Pipeline' tab, create new middleware and copy and paste this code. 12 | 4. Sign in to your Kenn IO account and get your API KEY and PROJECT ID. If you don't have an account, you can create one for free here https://keen.io/. Once you have your keys, add them to the middleware that you just created. You will also have to pick a name for your 'Event Collection' and change it in your middleware code. Hit Apply and Save. 13 | 5. Make a new request. 14 | 6. If everything went well, you should see the size of the request in your project on Keen IO. 15 | 16 | 17 | -------------------------------------------------------------------------------- /middleware/send-res-size-keen/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Send HTTP(s) response size to Keen IO", 3 | "description": "Sends the size of an HTTP(s) response to Keen IO", 4 | "files": ["send_res_size_keen.lua"], 5 | "author" : "vramosp", 6 | "email" : "vanessa@3scale.net", 7 | "github_user" : "vramosp", 8 | "version" : "1.0.0", 9 | "categories" : ["data", "keen_IO"], 10 | "endpoints": ["https://api.keen.io/3.0/"] 11 | } 12 | -------------------------------------------------------------------------------- /middleware/send-res-size-keen/send_res_size_keen.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | local response = next_middleware() 3 | local api_key = "YOUR-KEEN-IO-API-KEY-HERE" 4 | local size = base64.encode(json.encode({ size = #response.body })) 5 | http.get('https://api.keen.io/3.0/projects//events/?api_key=' .. api_key .. '&data=' .. size) 6 | return response 7 | end 8 | -------------------------------------------------------------------------------- /middleware/slow-request-github-issue/README.md: -------------------------------------------------------------------------------- 1 | # Slow request GitHub issue middleware 2 | 3 | Open a GitHub issue when a request takes too much time. If a slow request to the same URL occurs more than once the middleware updates the associated issue by reopening it. 4 | 5 | Default *threshold* is set to 1 second, you can update it in `slow_request_github_issue.lua`. 6 | 7 | ## Usage 8 | 9 | 1. Change `GITHUB_ACCESS_TOKEN` in `slow_request_github_issue.lua` with your GitHub API access token. 10 | 1. Change `GITHUB_REPO_FULL_NAME` in `slow_request_github_issue.lua` with your GitHub repository full name, e.g. 'leafo/lapis'. 11 | -------------------------------------------------------------------------------- /middleware/slow-request-github-issue/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slow request GitHub issue", 3 | "description": "Open a GitHub issue when a request takes too much time", 4 | "files": [ 5 | "slow_request_github_issue.lua" 6 | ], 7 | "spec": [ 8 | "slow_request_github_issue_spec.lua" 9 | ], 10 | "author": "Giovanni Cappellotto", 11 | "email": "potomak84@gmail.com", 12 | "version": "1.0.0", 13 | "categories": [ 14 | "devops", 15 | "notifications", 16 | "performances", 17 | "github" 18 | ], 19 | "endpoints": [ 20 | "*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /middleware/slow-request-github-issue/slow_request_github_issue.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | local threshold = 1.0 -- 1 second, for less use decimal numbers 3 | local github_access_token = 'GITHUB_ACCESS_TOKEN' 4 | local github_repo_full_name = 'GITHUB_REPO_FULL_NAME' 5 | 6 | local response = next_middleware() 7 | 8 | if trace.time > threshold then 9 | -- request.uri_full is used as key in the middleware bucket 10 | local issue_number = bucket.middleware.get(request.uri_full) 11 | 12 | local request_url = 'https://api.github.com/repos/' .. github_repo_full_name .. '/issues' 13 | local request_headers = {Authorization = 'token ' .. github_access_token} 14 | 15 | if issue_number == nil then 16 | -- create a new GitHub issue 17 | local request_body = {title = 'Slow request (' .. trace.time .. ' seconds): ' .. request.uri_full} 18 | local issue_response_body = http.simple({method = 'POST', url = request_url, headers = request_headers}, json.encode(request_body)) 19 | local issue = json.decode(issue_response_body) 20 | 21 | -- register issue number 22 | bucket.middleware.set(request.uri_full, issue.number) 23 | else 24 | -- update the GitHub issue (reopen) 25 | request_url = request_url .. '/' .. issue_number 26 | local request_body = {state = 'open'} 27 | http.simple({method = 'PATCH', url = request_url, headers = request_headers}, json.encode(request_body)) 28 | end 29 | end 30 | 31 | return response 32 | end 33 | -------------------------------------------------------------------------------- /middleware/slow-request-github-issue/slow_request_github_issue_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe('Slow request GitHub issue', function() 4 | local slow_request_github_issue 5 | before_each(function() 6 | slow_request_github_issue = spec.middleware('slow-request-github-issue/slow_request_github_issue.lua') 7 | end) 8 | 9 | describe('when the response is given quickly', function() 10 | it('does nothing', function() 11 | local request = spec.request({method = 'GET', uri = '/'}) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, {method = 'GET', uri = '/'}) 14 | spec.advance_time(0.2) -- less than threshold (1 second) 15 | return {status = 200, body = 'ok'} 16 | end) 17 | 18 | local response = slow_request_github_issue(request, next_middleware) 19 | 20 | assert.spy(next_middleware).was_called() 21 | assert.contains(response, {status = 200, body = 'ok'}) 22 | 23 | assert.equal(0, #spec.bucket.middleware:get_keys()) 24 | end) 25 | end) 26 | 27 | describe('when the response takes more than the threshold', function() 28 | describe('when it happens once', function() 29 | it('creates a GitHub issue and marks the middleware bucket', function() 30 | local request = spec.request({method = 'GET', uri = '/'}) 31 | local next_middleware = spec.next_middleware(function() 32 | assert.contains(request, {method = 'GET', uri = '/'}) 33 | spec.advance_time(2) -- 2 seconds 34 | return {status = 200, body = 'ok'} 35 | end) 36 | 37 | spec.mock_http({ 38 | method = 'POST', 39 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues', 40 | body = '{"title":"Slow request (2 seconds): http:\\/\\/localhost\\/"}', 41 | headers = {Authorization = 'token GITHUB_ACCESS_TOKEN'} 42 | }, { 43 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 44 | }) 45 | 46 | local response = slow_request_github_issue(request, next_middleware) 47 | 48 | assert.spy(next_middleware).was_called() 49 | assert.contains(response, {status = 200, body = 'ok'}) 50 | 51 | assert.truthy(spec.bucket.middleware.get('http://localhost/')) 52 | end) 53 | end) 54 | 55 | describe('when it happens more than once', function() 56 | it('reopens GitHub issue', function() 57 | local request = spec.request({method = 'GET', uri = '/'}) 58 | local next_middleware = spec.next_middleware(function() 59 | assert.contains(request, {method = 'GET', uri = '/'}) 60 | spec.advance_time(2) -- 2 seconds 61 | return {status = 200, body = 'ok'} 62 | end) 63 | 64 | spec.mock_http({ 65 | method = 'POST', 66 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues', 67 | body = '{"title":"Slow request (2 seconds): http:\\/\\/localhost\\/"}', 68 | headers = {Authorization = 'token GITHUB_ACCESS_TOKEN'} 69 | }, { 70 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 71 | }) 72 | 73 | spec.mock_http({ 74 | method = 'POST', -- FIXME: this really should be PATH, but http.simple is broken 75 | url = 'https://api.github.com/repos/GITHUB_REPO_FULL_NAME/issues/1', 76 | body = '{"state":"open"}', 77 | headers = {Authorization = 'token GITHUB_ACCESS_TOKEN'} 78 | }, { 79 | body = '{"url":"https://api.github.com/repos/owner/repo/issues/1","number":1}' 80 | }) 81 | 82 | slow_request_github_issue(request, next_middleware) 83 | spec.advance_time(2) -- 2 seconds 84 | slow_request_github_issue(request, next_middleware) -- twice 85 | 86 | assert.spy(next_middleware).was_called(2) 87 | end) 88 | end) 89 | 90 | end) 91 | end) 92 | -------------------------------------------------------------------------------- /middleware/twitter-oauth/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-oauth", 3 | "description": "Authenticate calls to Twitter API", 4 | "files": ["twitter_oauth.lua"], 5 | "spec": ["twitter_oauth_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["oauth"], 11 | "endpoints": ["https://api.twitter.com/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/twitter-oauth/twitter_oauth.lua: -------------------------------------------------------------------------------- 1 | return function (request, next_middleware) 2 | -- change to your own Twitter keys 3 | local api_key = "MY_TWITTER_API_KEY" 4 | local api_secret = "MY_TWITTER_API_SECRET" 5 | 6 | -- concatenate by ':' 7 | local str = api_key .. ':' .. api_secret 8 | -- console.log(str) 9 | 10 | -- generate base64 string 11 | local auth_header = "Basic ".. base64.encode(str) 12 | -- console.log(auth_header) 13 | 14 | -- headers to pass to /oauth2/ endpoint 15 | local headers_val ={} 16 | headers_val["Authorization"]=auth_header 17 | headers_val["Content-Type"]="application/x-www-form-urlencoded;charset=UTF-8" 18 | 19 | 20 | -- call to get access_token 21 | local body = http.simple{method='POST',url='https://api.twitter.com/oauth2/token',headers=headers_val, body={grant_type="client_credentials"}} 22 | local resp = json.decode(body) 23 | -- console.log("access_token",resp.access_token) 24 | 25 | 26 | -- pass the access_token to auth call 27 | request.headers.Authorization = "Bearer ".. resp.access_token 28 | 29 | return next_middleware() 30 | end 31 | -------------------------------------------------------------------------------- /middleware/twitter-oauth/twitter_oauth_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("Twitter-oauth", function() 4 | it("uses the api key + secret to call twitter API and include an Authorization token in the request headers", function() 5 | local twitter_oauth = spec.middleware('twitter-oauth/twitter_oauth.lua') 6 | local request = spec.request({ method = 'GET', uri = '/'}) 7 | local next_middleware = spec.next_middleware(function() 8 | assert.contains(request, { 9 | method = 'GET', 10 | uri = '/', 11 | headers = { Authorization = 'Bearer foo'} 12 | }) 13 | return {status = 200, body = 'ok'} 14 | end) 15 | 16 | spec.mock_http({ 17 | method = "POST", 18 | url = 'https://api.twitter.com/oauth2/token', 19 | body = 'grant%5Ftype=client%5Fcredentials', 20 | headers = { 21 | Authorization = "Basic TVlfVFdJVFRFUl9BUElfS0VZOk1ZX1RXSVRURVJfQVBJX1NFQ1JFVA==", 22 | ["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8", 23 | ["Content-type"] = "application/x-www-form-urlencoded", 24 | ["content-length"] = 33 25 | } 26 | }, { 27 | body = '{"access_token":"foo"}' 28 | }) 29 | 30 | local response = twitter_oauth(request, next_middleware) 31 | 32 | assert.spy(next_middleware).was_called() 33 | assert.contains(response, {status = 200, body = 'ok'}) 34 | end) 35 | end) 36 | -------------------------------------------------------------------------------- /middleware/uber-alert/README.md: -------------------------------------------------------------------------------- 1 | # Uber price alert 2 | 3 | ## How to use it 4 | 5 | 1. Define Uber API endpoint as you APItools service URL `https://api.uber.com/v1/` 6 | 2. Add Uber-passinfo middleware before in the pipeline 7 | 3. In PriceAlert middleware, change `max_price` to your desired price and `my_address` to with email address 8 | 4. Make call to http://YOUROWN.apitools.com/estimates/price to receive an email when uber price is above your limit -------------------------------------------------------------------------------- /middleware/uber-alert/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Uber - Price Alert", 3 | "description": "Get an alert when ride price reach a limit", 4 | "files": ["uber_pricealert.lua"], 5 | "spec": ["uber_pricealert_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["https://api.uber.com/v1/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/uber-alert/uber_pricealert.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | local max_price = 600 -- Maximum price to trigger alert 3 | local my_address = "YOUR_EMAIL_ADDRESS" 4 | 5 | local response = next_middleware() 6 | 7 | local result = json.decode(response.body) 8 | local prices = result.prices 9 | 10 | local message= "" 11 | local nb_results = 0 12 | 13 | for _,price in ipairs(prices) do 14 | 15 | if(max_price > tonumber(price.low_estimate)) then 16 | 17 | if (nb_results ==0) then --first result 18 | message = "You can go from " ..bucket.service.get("start_name").. " to "..bucket.service.get("end_name").. " for less than "..max_price..price.currency_code.. " on ".. price.display_name 19 | end 20 | 21 | nb_results = nb_results +1 22 | if(nb_results >1) then 23 | message= message .. " OR for less than "..max_price..price.currency_code.. " on ".. price.display_name 24 | end 25 | end 26 | end 27 | 28 | 29 | if (nb_results > 0) then 30 | send.mail(my_address,"Price alert on Uber",message) 31 | end 32 | 33 | return next_middleware() 34 | end -------------------------------------------------------------------------------- /middleware/uber-alert/uber_pricealert_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("burningman_categorize", function() 5 | 6 | it("Adds a bunch of stuff to the response", function() 7 | 8 | local backend_json = cjson.encode({ 9 | {name='foo', category='prty'}, 10 | {name='bar', category='prty'}, 11 | {name='baz', category='perf'}, 12 | }) 13 | 14 | local burningman_categorize = spec.middleware('burningman-categorize/burningman_categorize.lua') 15 | local request = spec.request({method = 'GET', uri = '/'}) 16 | local next_middleware = spec.next_middleware(function() 17 | assert.contains(request, { method = 'GET', uri = '/' }) 18 | return {status = 200, body = backend_json} 19 | end) 20 | 21 | local response = burningman_categorize(request, next_middleware) 22 | 23 | assert.spy(next_middleware).was_called() 24 | 25 | assert.contains(response, {status = 200 }) 26 | 27 | local info = cjson.decode(response.body) 28 | 29 | assert.same(info.parties, {{name='foo', category='prty'}, {name='bar', category='prty'}}) 30 | assert.same(info.performances, {{name='baz', category='perf'}}) 31 | assert.same(info.fire, {}) 32 | 33 | assert.equal(info.datainfo.nb_parties, 2) 34 | assert.equal(info.datainfo.nb_performances, 1) 35 | assert.equal(info.datainfo.nb_fire, 0) 36 | end) 37 | end) 38 | 39 | 40 | -------------------------------------------------------------------------------- /middleware/uber-passinfo/README.md: -------------------------------------------------------------------------------- 1 | # Uber price estimates 2 | 3 | ## How to use it 4 | 5 | 1. Define Uber API endpoint as you APItools service URL `https://api.uber.com/v1/` 6 | 2. Get a token from Uber and replace it in middleware 7 | 3. Make call to http://YOUROWN.apitools.com/estimates/price to get price estimates from points defined in middleware -------------------------------------------------------------------------------- /middleware/uber-passinfo/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Uber - passing params to API", 3 | "description": "Pass parameters to Uber API", 4 | "files": ["uber_passinfo.lua"], 5 | "spec": ["uber_passinfo_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["https://api.uber.com/v1/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/uber-passinfo/uber_passinfo.lua: -------------------------------------------------------------------------------- 1 | return function(request, next_middleware) 2 | local uber_token = "YOUR_UBER_TOKEN" 3 | local start_latitude = "37.6189" -- SFO airport 4 | local start_longitude = "-122.3750" 5 | local start_name = "SFO Airport" 6 | local end_latitude = "37.7833" -- Powell station 7 | local end_longitude = "-122.4167" 8 | local end_name = "SF downtown" 9 | 10 | -- store in bucket, names of start and end points 11 | bucket.service.set("start_name",start_name) 12 | bucket.service.set("end_name",end_name) 13 | 14 | 15 | request.args.server_token = uber_token 16 | request.args.start_latitude = start_latitude 17 | request.args.start_longitude = start_longitude 18 | request.args.end_latitude = end_latitude 19 | request.args.end_longitude = end_longitude 20 | 21 | local response = next_middleware() 22 | return response 23 | end -------------------------------------------------------------------------------- /middleware/uber-passinfo/uber_passinfo_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("burningman_categorize", function() 5 | 6 | it("Adds a bunch of stuff to the response", function() 7 | 8 | local backend_json = cjson.encode({ 9 | {name='foo', category='prty'}, 10 | {name='bar', category='prty'}, 11 | {name='baz', category='perf'}, 12 | }) 13 | 14 | local burningman_categorize = spec.middleware('burningman-categorize/burningman_categorize.lua') 15 | local request = spec.request({method = 'GET', uri = '/'}) 16 | local next_middleware = spec.next_middleware(function() 17 | assert.contains(request, { method = 'GET', uri = '/' }) 18 | return {status = 200, body = backend_json} 19 | end) 20 | 21 | local response = burningman_categorize(request, next_middleware) 22 | 23 | assert.spy(next_middleware).was_called() 24 | 25 | assert.contains(response, {status = 200 }) 26 | 27 | local info = cjson.decode(response.body) 28 | 29 | assert.same(info.parties, {{name='foo', category='prty'}, {name='bar', category='prty'}}) 30 | assert.same(info.performances, {{name='baz', category='perf'}}) 31 | assert.same(info.fire, {}) 32 | 33 | assert.equal(info.datainfo.nb_parties, 2) 34 | assert.equal(info.datainfo.nb_performances, 1) 35 | assert.equal(info.datainfo.nb_fire, 0) 36 | end) 37 | end) 38 | 39 | 40 | -------------------------------------------------------------------------------- /middleware/xml-to-json/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xml-to-json", 3 | "description": "Transforms an xml response into a JSON response", 4 | "files": ["xml-to-json.lua"], 5 | "spec": ["xml-to-json_spec.lua"], 6 | "author" : "3scale", 7 | "email" : "apitools@apitools.com", 8 | "github_user" : "apitools", 9 | "version" : "1.0.0", 10 | "categories" : ["misc"], 11 | "endpoints": ["*"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/xml-to-json/xml-to-json.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | -- 3 | -- This middleware transforms xml requests into json requests. 4 | -- 5 | -- Find more about it in the APItools blog: https://docs.apitools.com/blog/2014/05/06/transforming-an-rss-feed-into-json-with-apitools.html 6 | -- 7 | --]] 8 | 9 | local function parse_xml(xml_string) 10 | local root = {children = {}} 11 | local ancestors = {} 12 | 13 | local parser = xml.new({ 14 | StartElement = function(parser, tag, attrs) 15 | local node 16 | local parent = ancestors[#ancestors] 17 | if parent then 18 | node = {children = {}} 19 | parent.children[#parent.children + 1] = node 20 | else 21 | node = root 22 | end 23 | node.tag = tag 24 | node.attrs = attrs 25 | ancestors[#ancestors + 1] = node 26 | end, 27 | CharacterData = function(parser, str) 28 | local parent = ancestors[#ancestors] 29 | parent.children[#parent.children + 1] = str 30 | end, 31 | EndElement = function(parser, tag) 32 | ancestors[#ancestors] = nil 33 | end 34 | }) 35 | parser:parse(xml_string) 36 | 37 | return root 38 | end 39 | 40 | return function(request, next_middleware) 41 | local response = next_middleware() 42 | local root = parse_xml(response.body) 43 | 44 | response.body = json.encode(root) 45 | response.headers['Content-type'] = 'application/json' 46 | return response 47 | end 48 | -------------------------------------------------------------------------------- /middleware/xml-to-json/xml-to-json_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | local cjson = require 'cjson' 3 | 4 | describe("xml-to-json", function() 5 | it("transforms an xml response into json, sets the appropiate content-type header", function() 6 | local backend_xml = 'hellobye' 7 | 8 | local xml_to_json = spec.middleware('xml-to-json/xml-to-json.lua') 9 | local request = spec.request({ method = 'GET', uri = '/'}) 10 | local next_middleware = spec.next_middleware(function() 11 | assert.contains(request, {method='GET', uri = '/'}) 12 | return { 13 | status = 200, 14 | body = backend_xml, 15 | headers = {['Content-type'] = 'application/xml'} 16 | } 17 | end) 18 | 19 | local response = xml_to_json(request, next_middleware) 20 | 21 | assert.spy(next_middleware).was_called() 22 | assert.contains(response, {status = 200, headers = {['Content-type'] = 'application/json'}}) 23 | assert.same(cjson.decode(response.body), { 24 | attrs = {}, 25 | children = { { 26 | attrs = { 27 | ["1"] = "foo", 28 | foo = "bar" 29 | }, 30 | children = { "hello" }, 31 | tag = "a" 32 | }, { 33 | attrs = { 34 | ["1"] = "foo", 35 | foo = "bar" 36 | }, 37 | children = { "bye" }, 38 | tag = "b" 39 | } }, 40 | tag = "list" 41 | }) 42 | end) 43 | end) 44 | -------------------------------------------------------------------------------- /middleware/yo-api/README.md: -------------------------------------------------------------------------------- 1 | # Yo API middlware 2 | 3 | ## How to use it 4 | 5 | 1. Define Yo API endpoint as you APItools service URL `http://api.justyo.co/` 6 | 2. Define the Yo URL callback with your APItools URL `http://token.APITOOLS.com` 7 | 3. Add CORS-header middleware in the pipeline 8 | 4. Change `YO_API_TOKEN` in `yo.lua` by your own Yo API key. 9 | 5. Call `http://token.APITOOLS.com/yo` in your app to send individual Yos 10 | 11 | ## Tutorial 12 | More descriptive tutorial can be found [here](https://docs.apitools.com/2014/07/15/develop-your-first-yo-app-with-apitools.html) -------------------------------------------------------------------------------- /middleware/yo-api/apitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yo-api", 3 | "description": "Send YOs", 4 | "files": ["yo.lua"], 5 | "spec": ["yo_spec.lua"], 6 | "author" : "Picsoung", 7 | "email" : "nicolas@3scale.net", 8 | "github_user" : "Picsoung", 9 | "version" : "1.0.0", 10 | "categories" : ["APIs"], 11 | "endpoints": ["http://api.justyo.co/"] 12 | } 13 | -------------------------------------------------------------------------------- /middleware/yo-api/yo.lua: -------------------------------------------------------------------------------- 1 | -- split function to split a string by a delimiter 2 | local function split(s, delimiter) 3 | local result = {} 4 | for match in (s..delimiter):gmatch("(.-)"..delimiter) do 5 | table.insert(result, match) 6 | end 7 | return result 8 | end 9 | 10 | return function(request, next_middleware) 11 | local apiToken = 'YO_API_TOKEN' 12 | request.headers['Content-Type'] = 'application/json' 13 | 14 | -- endpoint to send an individual yo is called 15 | if request.uri == '/yo/' then 16 | local body = request.body 17 | 18 | local yoUsername = split(body,'=')[2] 19 | -- console.log(yoUsername) 20 | 21 | request.body = '{"username":"'.. string.upper(yoUsername) .. '","api_token":"' .. apiToken .. '"}' 22 | 23 | -- endpoint to Yo all is called 24 | elseif request.uri =='/yoall/' then 25 | 26 | request.body = '{"api_token":"' .. apiToken .. '"}' 27 | 28 | -- callback url is called 29 | elseif request.uri == '/' then 30 | send.mail('me@email.com','New Yo subscriber', 'NEW Yo SUBSCRIBER ' .. request.args.username) 31 | send.notification({msg="new subscriber " .. request.args.username, level='info'}) 32 | end 33 | 34 | -- console.log(request.body) 35 | 36 | return next_middleware() 37 | end 38 | -------------------------------------------------------------------------------- /middleware/yo-api/yo_spec.lua: -------------------------------------------------------------------------------- 1 | local spec = require 'spec.spec' 2 | 3 | describe("yo-api", function() 4 | local yo 5 | before_each(function() 6 | yo = spec.middleware("yo-api/yo.lua") 7 | end) 8 | 9 | describe("when the uri is /", function() 10 | it("sends an email and a notification", function() 11 | local request = spec.request({ method = 'GET', url = '/?username=peter' }) 12 | local next_middleware = spec.next_middleware(function() 13 | assert.contains(request, { 14 | method = 'GET', 15 | uri = '/', 16 | query='username=peter', 17 | headers = {['Content-Type'] = 'application/json'} 18 | }) 19 | return {status = 200, body = 'ok'} 20 | end) 21 | 22 | local response = yo(request, next_middleware) 23 | 24 | assert.spy(next_middleware).was_called() 25 | assert.contains(response, {status = 200, body = 'ok'}) 26 | 27 | assert.equal(#spec.sent.emails, 1) 28 | 29 | local last_email = spec.sent.emails.last 30 | assert.equal('me@email.com', last_email.to) 31 | assert.equal('New Yo subscriber', last_email.subject) 32 | assert.equal('NEW Yo SUBSCRIBER peter', last_email.message) 33 | 34 | assert.equal(#spec.sent.events, 1) 35 | 36 | local last_event = spec.sent.events.last 37 | assert.same({channel='middleware', level='info', msg='new subscriber peter'}, last_event) 38 | end) 39 | end) 40 | 41 | describe("then the uri is /yoall/", function() 42 | it("passes the apitoken to the backend", function() 43 | 44 | local request = spec.request({ method = 'GET', uri = '/yoall/'}) 45 | local next_middleware = spec.next_middleware(function() 46 | assert.contains(request, { 47 | method = 'GET', 48 | uri = '/yoall/', 49 | headers = {['Content-Type'] = 'application/json'}, 50 | body = '{"api_token":"YO_API_TOKEN"}' 51 | }) 52 | return {status = 200, body = 'ok'} 53 | end) 54 | 55 | local response = yo(request, next_middleware) 56 | assert.spy(next_middleware).was_called() 57 | assert.contains(response, {status = 200, body = 'ok'}) 58 | 59 | assert.equal(#spec.sent.emails, 0) 60 | assert.equal(#spec.sent.events, 0) 61 | end) 62 | end) 63 | 64 | describe("when the request is /yo/", function() 65 | it("passes & uppercases the username to the backend, plus the API token", function() 66 | local request = spec.request({ method = 'GET', uri = '/yo/', body="username=peter"}) 67 | local next_middleware = spec.next_middleware(function() 68 | assert.contains(request, { 69 | method = 'GET', 70 | uri = '/yo/', 71 | headers = {['Content-Type'] = 'application/json'}, 72 | body = '{"username":"PETER","api_token":"YO_API_TOKEN"}' 73 | }) 74 | return {status = 200, body = 'ok'} 75 | end) 76 | 77 | local response = yo(request, next_middleware) 78 | assert.spy(next_middleware).was_called() 79 | assert.contains(response, {status = 200, body = 'ok'}) 80 | 81 | assert.equal(#spec.sent.emails, 0) 82 | assert.equal(#spec.sent.events, 0) 83 | end) 84 | end) 85 | end) 86 | -------------------------------------------------------------------------------- /spec/assert_contains.lua: -------------------------------------------------------------------------------- 1 | local s = require("say") 2 | local ass = require("luassert") 3 | 4 | local function contains(container, contained) 5 | if container == contained then return true end 6 | local t1,t2 = type(container), type(contained) 7 | if t1 ~= t2 then return false end 8 | 9 | if t1 == 'table' then 10 | for k,v in pairs(contained) do 11 | if not contains(container[k], v) then return false end 12 | end 13 | return true 14 | end 15 | return false 16 | end 17 | 18 | local function contains_for_luassert(state, arguments) 19 | return contains(arguments[1], arguments[2]) 20 | end 21 | 22 | s:set("assertion.contains.positive", "Expected %s\n to contain \n%s") 23 | s:set("assertion.contains.negative", "Expected %s\n to NOT contain \n%s") 24 | 25 | ass:register("assertion", "contains", contains_for_luassert, "assertion.contains.positive", "assertion.contains.negative") 26 | -------------------------------------------------------------------------------- /spec/assert_difference.lua: -------------------------------------------------------------------------------- 1 | local s = require("say") 2 | local ass = require("luassert") 3 | 4 | local default_diff = 0.0001 5 | 6 | local function difference(a,b, diff) 7 | diff = diff or default_diff 8 | if a == b then return true end 9 | local t1,t2 = type(a), type(b) 10 | if t1 ~= t2 then return false end 11 | 12 | if t1 == 'number' then 13 | return math.abs(a-b) < diff 14 | end 15 | return false 16 | end 17 | 18 | local function difference_for_luassert(state, arguments) 19 | return difference(arguments[1], arguments[2], arguments[3]) 20 | end 21 | 22 | s:set("assertion.difference.positive", "Expected the difference between %s and %s to be smaller than %s") 23 | s:set("assertion.difference.negative", "Expected the difference between %s and %s to NOT to be smaller than %s") 24 | 25 | ass:register("assertion", "difference", difference_for_luassert, "assertion.difference.positive", "assertion.difference.negative") 26 | -------------------------------------------------------------------------------- /spec/env/bucket.lua: -------------------------------------------------------------------------------- 1 | local bucket = {} 2 | 3 | 4 | ---------------------------------------- 5 | 6 | local Bucket = {} 7 | local Bucket_methods = {} 8 | 9 | function Bucket_methods:get(field_name) 10 | local exptime = self.expirations[field_name] 11 | if exptime and exptime > self.time.now() then 12 | return self.values[field_name] 13 | else 14 | self.expirations[field_name] = nil 15 | self.values[field_name] = nil 16 | end 17 | end 18 | 19 | function Bucket_methods:set(field_name, value, exptime) 20 | exptime = exptime or math.huge 21 | self.expirations[field_name] = exptime 22 | self.values[field_name] = value 23 | end 24 | 25 | function Bucket_methods:delete(field_name) 26 | self.values[field_name] = nil 27 | end 28 | 29 | function Bucket_methods:incr(field_name, amount) 30 | self.values[field_name] = self.values[field_name] + (amount or 1) 31 | end 32 | 33 | function Bucket_methods:add(field_name, value, exptime) 34 | if self.get(field_name) then return false end 35 | self.set(field_name, value, exptime) 36 | return true 37 | end 38 | 39 | function Bucket_methods:get_keys() 40 | local keys = {} 41 | for k in pairs(self.values) do 42 | keys[#keys + 1] = k 43 | end 44 | return keys 45 | end 46 | 47 | ---------------------------------------- 48 | 49 | local function bucketIndex(bucket, name) 50 | local method = Bucket_methods[name] 51 | if method then 52 | local f = function(...) return method(bucket, ...) end 53 | rawset(bucket, name, f) 54 | return f 55 | end 56 | end 57 | 58 | local Bucket_mt = { __index = bucketIndex } 59 | 60 | function Bucket.new(time) 61 | return setmetatable({time = time, values = {}, expirations = {}}, Bucket_mt) 62 | end 63 | 64 | --------------------------------------- 65 | 66 | function bucket.new(spec, time) 67 | local instance = spec.bucket or { middleware = Bucket.new(time), service = Bucket.new(time) } 68 | spec.bucket = instance 69 | return instance 70 | end 71 | 72 | return bucket 73 | -------------------------------------------------------------------------------- /spec/env/console.lua: -------------------------------------------------------------------------------- 1 | local inspect = require 'spec.env.inspect' 2 | 3 | local Console = {} 4 | 5 | local function log(level, ...) 6 | local args = {...} 7 | if #args == 1 then args = args[1] end 8 | if type(args) ~= 'string' then args = inspect(args) end 9 | 10 | print(('Console %s: %s'):format(level, args)) 11 | end 12 | 13 | function Console.new() 14 | 15 | return { 16 | log = function(...) 17 | log('Log', ...) 18 | end, 19 | debug = function(...) 20 | log('Debug', ...) 21 | end, 22 | info = function(...) 23 | log('Info', ...) 24 | end, 25 | warn = function(...) 26 | log('Warn', ...) 27 | end, 28 | error = function(...) 29 | log('Error', ...) 30 | end 31 | } 32 | end 33 | 34 | return Console 35 | -------------------------------------------------------------------------------- /spec/env/env.lua: -------------------------------------------------------------------------------- 1 | local inspect = require 'spec.env.inspect' 2 | local http = require 'spec.env.http' 3 | local sha = require 'spec.env.sha' 4 | local bucket = require 'spec.env.bucket' 5 | local Console = require 'spec.env.console' 6 | local send = require 'spec.env.send' 7 | local metric = require 'spec.env.metric' 8 | local time = require 'spec.env.time' 9 | 10 | local xml = require 'lxp' -- luarocks install LuaExpat 11 | local cjson = require 'cjson' -- luarocks install lua-cjson 12 | local mime = require 'mime' -- luarocks install luasocket 13 | 14 | local env = {} 15 | 16 | function env.new(spec) 17 | 18 | local base64 = { decode = mime.unb64, 19 | encode = mime.b64 } 20 | 21 | local hmac = { sha256 = sha.hash256 } 22 | 23 | local console = Console.new() 24 | 25 | local trace = { link = "", time = 0 } 26 | 27 | local t = time.new(spec) 28 | local b = bucket.new(spec, t) 29 | local s = send.new(spec) 30 | local m = metric.new(spec) 31 | local h = http.new(spec) 32 | 33 | spec.trace = trace 34 | spec.time = t 35 | 36 | return { 37 | 38 | console = console, 39 | inspect = inspect, 40 | 41 | log = print, 42 | base64 = base64, 43 | hmac = hmac, 44 | trace = trace, 45 | json = cjson, 46 | xml = xml, 47 | time = t, 48 | bucket = b, 49 | send = s, 50 | metric = m, 51 | http = h 52 | } 53 | 54 | end 55 | 56 | 57 | return env 58 | -------------------------------------------------------------------------------- /spec/env/http.lua: -------------------------------------------------------------------------------- 1 | local inspect = require 'spec.env.inspect' 2 | local http_utils = require 'spec.env.http_utils' 3 | local querystring = require 'spec.env.querystring' 4 | 5 | local http = {} 6 | 7 | local function same(a,b) 8 | local ta,tb = type(a), type(b) 9 | if ta ~= tb then return false end 10 | if ta ~= 'table' then return a == b end 11 | 12 | for k,v in pairs(a) do 13 | if not same(v, b[k]) then return false end 14 | end 15 | return true 16 | end 17 | 18 | local function get_mocked_response_for(req, spec) 19 | local http_mocks = spec.http_mocks or {} 20 | for i=1, #http_mocks do 21 | local http_mock = spec.http_mocks[i] 22 | if same(http_mock.request, req) then 23 | return http_mock.response 24 | end 25 | end 26 | error(("Attempted to make the http request:\n%s\nBut it was not found. Existing mocks: \n%s"):format(inspect(req), inspect(http_mocks))) 27 | end 28 | 29 | local function complete_request(req, body) 30 | req = type(req) == "string" and { url = req } or req 31 | req = http_utils.complete_request(req) 32 | 33 | if body then 34 | req.method = "POST" 35 | req.body = body 36 | req.headers["content-length"] = #body 37 | end 38 | 39 | if type(req.body) == "table" then 40 | req.body = querystring.encode(req.body) 41 | req.headers["Content-type"] = "application/x-www-form-urlencoded" 42 | req.headers["content-length"] = #req.body 43 | end 44 | 45 | return req 46 | end 47 | 48 | -- returns a table instead of 3 elements, like http.simple, which is bonkers 49 | local function sane_simple(req, body, spec) 50 | req = complete_request(req, body) 51 | return get_mocked_response_for(req, spec) 52 | end 53 | 54 | -- 55 | 56 | function http.new(spec) 57 | local instance = {} 58 | 59 | instance.simple = function(req, body) 60 | local res = sane_simple(req, body, spec) 61 | return res.body, res.status, res.header 62 | end 63 | 64 | instance.multi = function(reqs) 65 | local result = {} 66 | for i=1, #reqs do 67 | result[i] = sane_simple(reqs[i], nil, spec) 68 | end 69 | return result 70 | end 71 | 72 | instance.is_success = function(status) 73 | return status >= 200 and status < 300 74 | end 75 | 76 | return instance 77 | end 78 | 79 | return http 80 | -------------------------------------------------------------------------------- /spec/env/http_utils.lua: -------------------------------------------------------------------------------- 1 | local querystring = require 'spec.env.querystring' 2 | local url = require 'socket.url' -- provided by luasocket 3 | 4 | local http_utils = {} 5 | 6 | --------------------------------- 7 | 8 | local function copy_recursive(t) 9 | if type(t) ~= 'table' then return t end 10 | local c = {} 11 | for k,v in pairs(t) do 12 | c[copy_recursive(k)] = copy_recursive(v) 13 | end 14 | setmetatable(c, getmetatable(t)) 15 | return c 16 | end 17 | 18 | --------------------------------- 19 | 20 | function http_utils.complete_request(req) 21 | req = copy_recursive(req) 22 | 23 | local parseable_uri = req.url or req.uri 24 | if parseable_uri then 25 | local info = url.parse(parseable_uri) 26 | req.scheme = req.scheme or info.scheme or 'http' 27 | req.host = req.host or info.host or 'localhost' 28 | req.uri = info.path:gsub('%?.*', '') 29 | local query = req.query or info.query or '' 30 | req.args = req.args or querystring.parse(query) 31 | req.query = req.query or querystring.encode(req.args) 32 | local has_query = query == '' and '' or '?' 33 | req.uri_relative = req.uri .. has_query .. req.query 34 | req.uri_full = req.scheme .. '://' .. req.host .. req.uri_relative 35 | req.url = req.uri_full 36 | else 37 | error("Invalid request: Must provide at least a url or uri") 38 | end 39 | 40 | req.method = req.method or 'GET' 41 | req.headers = req.headers or {} 42 | req.body = req.body or "" 43 | 44 | return req 45 | end 46 | 47 | function http_utils.complete_response(res) 48 | res = copy_recursive(res) 49 | 50 | res.status = res.status or 200 51 | res.body = res.body or 'ok (default body from spec.lua)' 52 | res.headers = res.headers or {} 53 | 54 | return res 55 | end 56 | 57 | return http_utils 58 | -------------------------------------------------------------------------------- /spec/env/inspect.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------------------------------------------------- 2 | -- inspect.lua - v1.2.1 (2013-01) 3 | -- Enrique García Cota - enrique.garcia.cota [AT] gmail [DOT] com 4 | -- human-readable representations of tables. 5 | -- inspired by http://lua-users.org/wiki/TableSerialization 6 | ----------------------------------------------------------------------------------------------------------------------- 7 | 8 | local inspect ={} 9 | inspect.__VERSION = '1.2.0' 10 | 11 | -- Apostrophizes the string if it has quotes, but not aphostrophes 12 | -- Otherwise, it returns a regular quoted string 13 | local function smartQuote(str) 14 | if string.match( string.gsub(str,"[^'\"]",""), '^"+$' ) then 15 | return "'" .. str .. "'" 16 | end 17 | return string.format("%q", str ) 18 | end 19 | 20 | local controlCharsTranslation = { 21 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 22 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\\"] = "\\\\" 23 | } 24 | 25 | local function unescapeChar(c) return controlCharsTranslation[c] end 26 | 27 | local function unescape(str) 28 | local result, _ = string.gsub( str, "(%c)", unescapeChar ) 29 | return result 30 | end 31 | 32 | local function isIdentifier(str) 33 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 34 | end 35 | 36 | local function isArrayKey(k, length) 37 | return type(k)=='number' and 1 <= k and k <= length 38 | end 39 | 40 | local function isDictionaryKey(k, length) 41 | return not isArrayKey(k, length) 42 | end 43 | 44 | local sortOrdersByType = { 45 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 46 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 47 | } 48 | 49 | local function sortKeys(a,b) 50 | local ta, tb = type(a), type(b) 51 | if ta ~= tb then return sortOrdersByType[ta] < sortOrdersByType[tb] end 52 | if ta == 'string' or ta == 'number' then return a < b end 53 | return false 54 | end 55 | 56 | local function getDictionaryKeys(t) 57 | local length = #t 58 | local keys = {} 59 | for k,_ in pairs(t) do 60 | if isDictionaryKey(k, length) then table.insert(keys,k) end 61 | end 62 | table.sort(keys, sortKeys) 63 | return keys 64 | end 65 | 66 | local function getToStringResultSafely(t, mt) 67 | local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') 68 | local string, status 69 | if type(__tostring) == 'function' then 70 | status, string = pcall(__tostring, t) 71 | string = status and string or 'error: ' .. tostring(string) 72 | end 73 | return string 74 | end 75 | 76 | local Inspector = {} 77 | 78 | function Inspector:new(t, depth) 79 | local inspector = { 80 | buffer = {}, 81 | depth = depth, 82 | level = 0, 83 | maxIds = { 84 | ['function'] = 0, 85 | ['userdata'] = 0, 86 | ['thread'] = 0, 87 | ['table'] = 0 88 | }, 89 | ids = { 90 | ['function'] = setmetatable({}, {__mode = "kv"}), 91 | ['userdata'] = setmetatable({}, {__mode = "kv"}), 92 | ['thread'] = setmetatable({}, {__mode = "kv"}), 93 | ['table'] = setmetatable({}, {__mode = "kv"}) 94 | }, 95 | tableAppearances = setmetatable({}, {__mode = "k"}) 96 | } 97 | 98 | setmetatable(inspector, {__index = Inspector}) 99 | 100 | inspector:countTableAppearances(t) 101 | 102 | return inspector:putValue(t) 103 | end 104 | 105 | function Inspector:countTableAppearances(t) 106 | if type(t) == 'table' then 107 | if not self.tableAppearances[t] then 108 | self.tableAppearances[t] = 1 109 | for k,v in pairs(t) do 110 | self:countTableAppearances(k) 111 | self:countTableAppearances(v) 112 | end 113 | self:countTableAppearances(getmetatable(t)) 114 | else 115 | self.tableAppearances[t] = self.tableAppearances[t] + 1 116 | end 117 | end 118 | end 119 | 120 | function Inspector:tabify() 121 | self:puts("\n", string.rep(" ", self.level)) 122 | return self 123 | end 124 | 125 | function Inspector:up() 126 | self.level = self.level - 1 127 | end 128 | 129 | function Inspector:down() 130 | self.level = self.level + 1 131 | end 132 | 133 | function Inspector:puts(...) 134 | local args = {...} 135 | local len = #self.buffer 136 | for i=1, #args do 137 | len = len + 1 138 | self.buffer[len] = tostring(args[i]) 139 | end 140 | return self 141 | end 142 | 143 | function Inspector:commaControl(comma) 144 | if comma then self:puts(',') end 145 | return true 146 | end 147 | 148 | function Inspector:putTable(t) 149 | if self:alreadyVisited(t) then 150 | self:puts('') 151 | elseif self.depth and self.level >= self.depth then 152 | self:puts('{...}') 153 | else 154 | if self.tableAppearances[t] > 1 then 155 | self:puts('<',self:getId(t),'>') 156 | end 157 | self:puts('{') 158 | self:down() 159 | 160 | local length = #t 161 | local mt = getmetatable(t) 162 | 163 | local string = getToStringResultSafely(t, mt) 164 | if type(string) == 'string' and #string > 0 then 165 | self:puts(' -- ', unescape(string)) 166 | if length >= 1 then self:tabify() end -- tabify the array values 167 | end 168 | 169 | local comma = false 170 | for i=1, length do 171 | comma = self:commaControl(comma) 172 | self:puts(' '):putValue(t[i]) 173 | end 174 | 175 | local dictKeys = getDictionaryKeys(t) 176 | 177 | for _,k in ipairs(dictKeys) do 178 | comma = self:commaControl(comma) 179 | self:tabify():putKey(k):puts(' = '):putValue(t[k]) 180 | end 181 | 182 | if mt then 183 | comma = self:commaControl(comma) 184 | self:tabify():puts(' = '):putValue(mt) 185 | end 186 | 187 | self:up() 188 | 189 | if #dictKeys > 0 or mt then -- dictionary table. Justify closing } 190 | self:tabify() 191 | elseif length > 0 then -- array tables have one extra space before closing } 192 | self:puts(' ') 193 | end 194 | self:puts('}') 195 | end 196 | return self 197 | end 198 | 199 | function Inspector:alreadyVisited(v) 200 | return self.ids[type(v)][v] ~= nil 201 | end 202 | 203 | function Inspector:getId(v) 204 | local tv = type(v) 205 | local id = self.ids[tv][v] 206 | if not id then 207 | id = self.maxIds[tv] + 1 208 | self.maxIds[tv] = id 209 | self.ids[tv][v] = id 210 | end 211 | return id 212 | end 213 | 214 | function Inspector:putValue(v) 215 | local tv = type(v) 216 | 217 | if tv == 'string' then 218 | self:puts(smartQuote(unescape(v))) 219 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then 220 | self:puts(tostring(v)) 221 | elseif tv == 'table' then 222 | self:putTable(v) 223 | else 224 | self:puts('<',tv,' ',self:getId(v),'>') 225 | end 226 | return self 227 | end 228 | 229 | function Inspector:putKey(k) 230 | if isIdentifier(k) then return self:puts(k) end 231 | return self:puts( "[" ):putValue(k):puts("]") 232 | end 233 | 234 | function Inspector:tostring() 235 | return table.concat(self.buffer) 236 | end 237 | 238 | setmetatable(inspect, { __call = function(_,t,depth) 239 | return Inspector:new(t, depth):tostring() 240 | end }) 241 | 242 | return inspect 243 | 244 | -------------------------------------------------------------------------------- /spec/env/metric.lua: -------------------------------------------------------------------------------- 1 | local metric = {} 2 | 3 | function metric.new(spec) 4 | 5 | local instance = spec.metric or { counts = {}, sets = {} } 6 | spec.metric = instance 7 | 8 | instance.count = function(name, inc) 9 | instance.counts[name] = (instance.counts[name] or 0) + (inc or 1) 10 | end 11 | 12 | instance.set = function(name, value) 13 | instance.sets[name] = value 14 | end 15 | 16 | return instance 17 | end 18 | 19 | return metric 20 | -------------------------------------------------------------------------------- /spec/env/querystring.lua: -------------------------------------------------------------------------------- 1 | -- query string module. inspired in luacgi code -- 2 | local string = require ("string") 3 | 4 | local QueryString = {} 5 | 6 | local function escapeURI(s) 7 | s = string.gsub (s, "\n", "\r\n") 8 | s = string.gsub (s, "([^0-9a-zA-Z ])", -- locale independent 9 | function (c) return string.format ("%%%02X", string.byte(c)) end) 10 | s = string.gsub (s, " ", "+") 11 | return s 12 | end 13 | 14 | local function binsert(buffer, blen, str) 15 | blen = blen + 1 16 | buffer[blen] = str 17 | return blen 18 | end 19 | 20 | local function stringifyPrimitive(p) 21 | local t = type(p) 22 | if t == "boolean" or t == "number" then 23 | return tostring(p) 24 | elseif t == "string" then 25 | return escapeURI(p) 26 | else 27 | error("Unknown type to stringify: " .. t .. "(" .. tostring(s) .. ")") 28 | end 29 | end 30 | 31 | local function qinsert(args, str) 32 | local first = str:find("=") 33 | if first then 34 | args[str:sub(0, first-1)] = str:sub(first+1) 35 | end 36 | end 37 | 38 | local function get_keys(t) 39 | local keys, klen = {}, 0 40 | for k in pairs(t) do 41 | klen = klen + 1 42 | keys[klen] = k 43 | end 44 | return keys, klen 45 | end 46 | 47 | ----------------------------- 48 | 49 | function QueryString.escape(s) 50 | return escapeURI(s) 51 | end 52 | 53 | function QueryString.unescapeBuffer(s) 54 | s = string.gsub (s, "+", " ") 55 | s = string.gsub (s, "%%(%x%x)", 56 | function(h) 57 | return string.char(tonumber(h,16)) 58 | end) 59 | s = string.gsub (s, "\r\n", "\n") 60 | return s 61 | end 62 | 63 | function QueryString.unescape(s, decodeSpaces) 64 | return QueryString.unescapeBuffer(s) -- ignored decodeSpaces) 65 | end 66 | 67 | function QueryString.parse(query) 68 | local args, pos = {}, 0 69 | 70 | -- XXX this is wrong 71 | query = query:gsub("&", "&") 72 | query = query:gsub("<", "<") 73 | query = query:gsub(">", ">") 74 | 75 | while true do 76 | local first, last = query:find("&", pos) 77 | if first then 78 | qinsert(args, query:sub(pos, first-1)); 79 | pos = last+1 80 | else 81 | qinsert(args, query:sub(pos)); 82 | break; 83 | end 84 | end 85 | return args 86 | end 87 | 88 | QueryString.decode = QueryString.parse 89 | 90 | function QueryString.stringify(obj, sep, eq) 91 | sep = sep or '&' 92 | eq = eq or '=' 93 | 94 | local keys, klen = get_keys(obj) 95 | if klen == 0 then return '' end 96 | table.sort(keys) 97 | 98 | local buf, blen = {}, 0 99 | 100 | local k,v 101 | for i=1, klen do 102 | k = keys[i] 103 | v = obj[k] 104 | if i > 1 then blen = binsert(buf, blen, sep) end 105 | blen = binsert(buf, blen, stringifyPrimitive(k)) 106 | blen = binsert(buf, blen, eq) 107 | blen = binsert(buf, blen, stringifyPrimitive(v)) 108 | end 109 | 110 | return table.concat(buf) 111 | end 112 | 113 | QueryString.encode = QueryString.stringify 114 | 115 | return QueryString 116 | -------------------------------------------------------------------------------- /spec/env/send.lua: -------------------------------------------------------------------------------- 1 | local send = {} 2 | 3 | function send.new(spec) 4 | local sent = spec.sent or {emails = {}, events = {}} 5 | 6 | spec.sent = sent 7 | 8 | local instance = {sent = sent} 9 | 10 | instance.email = function(to, subject, message) 11 | local email = {to=to, subject=subject, message = message} 12 | sent.emails[#sent.emails + 1] = email 13 | sent.emails.last = email 14 | end 15 | 16 | instance.mail = instance.email 17 | 18 | instance.event = function(ev) 19 | sent.events[#sent.events + 1] = ev 20 | sent.events.last = ev 21 | end 22 | 23 | instance.notification = function(notification) 24 | notification.channel = 'middleware' 25 | instance.event(notification) 26 | end 27 | 28 | return instance 29 | end 30 | 31 | return send 32 | -------------------------------------------------------------------------------- /spec/env/sha.lua: -------------------------------------------------------------------------------- 1 | -- SHA-256 code in Lua 5.2; based on the pseudo-code from 2 | -- Wikipedia (http://en.wikipedia.org/wiki/SHA-2) 3 | 4 | -- copied from http://lua-users.org/wiki/SecureHashAlgorithm, adepted to luaJIT by using bit instead of bit32 5 | 6 | local bit = require 'bit' 7 | 8 | 9 | local band, rrotate, bxor, rshift, bnot = 10 | bit.band, bit.ror, bit.bxor, bit.rshift, bit.bnot 11 | 12 | -- Initialize table of round constants 13 | -- (first 32 bits of the fractional parts of the cube roots of the first 14 | -- 64 primes 2..311): 15 | local k = { 16 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 17 | 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 18 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 19 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 20 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 21 | 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 22 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 23 | 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 24 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 25 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 26 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 27 | 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 28 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 29 | 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 30 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 31 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, 32 | } 33 | 34 | 35 | -- transform a string of bytes in a string of hexadecimal digits 36 | local function str2hexa (s) 37 | local h = string.gsub(s, ".", function(c) 38 | return string.format("%02x", string.byte(c)) 39 | end) 40 | return h 41 | end 42 | 43 | 44 | -- transform number 'l' in a big-endian sequence of 'n' bytes 45 | -- (coded as a string) 46 | local function num2s (l, n) 47 | local s = "" 48 | for i = 1, n do 49 | local rem = l % 256 50 | s = string.char(rem) .. s 51 | l = (l - rem) / 256 52 | end 53 | return s 54 | end 55 | 56 | -- transform the big-endian sequence of four bytes starting at 57 | -- index 'i' in 's' into a number 58 | local function s232num (s, i) 59 | local n = 0 60 | for i = i, i + 3 do 61 | n = n*256 + string.byte(s, i) 62 | end 63 | return n 64 | end 65 | 66 | 67 | -- append the bit '1' to the message 68 | -- append k bits '0', where k is the minimum number >= 0 such that the 69 | -- resulting message length (in bits) is congruent to 448 (mod 512) 70 | -- append length of message (before pre-processing), in bits, as 64-bit 71 | -- big-endian integer 72 | local function preproc (msg, len) 73 | local extra = 64 - ((len + 1 + 8) % 64) 74 | len = num2s(8 * len, 8) -- original len in bits, coded 75 | msg = msg .. "\128" .. string.rep("\0", extra) .. len 76 | assert(#msg % 64 == 0) 77 | return msg 78 | end 79 | 80 | 81 | local function initH224 (H) 82 | -- (second 32 bits of the fractional parts of the square roots of the 83 | -- 9th through 16th primes 23..53) 84 | H[1] = 0xc1059ed8 85 | H[2] = 0x367cd507 86 | H[3] = 0x3070dd17 87 | H[4] = 0xf70e5939 88 | H[5] = 0xffc00b31 89 | H[6] = 0x68581511 90 | H[7] = 0x64f98fa7 91 | H[8] = 0xbefa4fa4 92 | return H 93 | end 94 | 95 | 96 | local function initH256 (H) 97 | -- (first 32 bits of the fractional parts of the square roots of the 98 | -- first 8 primes 2..19): 99 | H[1] = 0x6a09e667 100 | H[2] = 0xbb67ae85 101 | H[3] = 0x3c6ef372 102 | H[4] = 0xa54ff53a 103 | H[5] = 0x510e527f 104 | H[6] = 0x9b05688c 105 | H[7] = 0x1f83d9ab 106 | H[8] = 0x5be0cd19 107 | return H 108 | end 109 | 110 | 111 | local function digestblock (msg, i, H) 112 | 113 | -- break chunk into sixteen 32-bit big-endian words w[1..16] 114 | local w = {} 115 | for j = 1, 16 do 116 | w[j] = s232num(msg, i + (j - 1)*4) 117 | end 118 | 119 | -- Extend the sixteen 32-bit words into sixty-four 32-bit words: 120 | for j = 17, 64 do 121 | local v = w[j - 15] 122 | local s0 = bxor(rrotate(v, 7), rrotate(v, 18), rshift(v, 3)) 123 | v = w[j - 2] 124 | local s1 = bxor(rrotate(v, 17), rrotate(v, 19), rshift(v, 10)) 125 | w[j] = w[j - 16] + s0 + w[j - 7] + s1 126 | end 127 | 128 | -- Initialize hash value for this chunk: 129 | local a, b, c, d, e, f, g, h = 130 | H[1], H[2], H[3], H[4], H[5], H[6], H[7], H[8] 131 | 132 | -- Main loop: 133 | for i = 1, 64 do 134 | local s0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22)) 135 | local maj = bxor(band(a, b), band(a, c), band(b, c)) 136 | local t2 = s0 + maj 137 | local s1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25)) 138 | local ch = bxor (band(e, f), band(bnot(e), g)) 139 | local t1 = h + s1 + ch + k[i] + w[i] 140 | 141 | h = g 142 | g = f 143 | f = e 144 | e = d + t1 145 | d = c 146 | c = b 147 | b = a 148 | a = t1 + t2 149 | end 150 | 151 | -- Add (mod 2^32) this chunk's hash to result so far: 152 | H[1] = band(H[1] + a) 153 | H[2] = band(H[2] + b) 154 | H[3] = band(H[3] + c) 155 | H[4] = band(H[4] + d) 156 | H[5] = band(H[5] + e) 157 | H[6] = band(H[6] + f) 158 | H[7] = band(H[7] + g) 159 | H[8] = band(H[8] + h) 160 | 161 | end 162 | 163 | 164 | local function finalresult224 (H) 165 | -- Produce the final hash value (big-endian): 166 | return 167 | str2hexa(num2s(H[1], 4)..num2s(H[2], 4)..num2s(H[3], 4)..num2s(H[4], 4).. 168 | num2s(H[5], 4)..num2s(H[6], 4)..num2s(H[7], 4)) 169 | end 170 | 171 | 172 | local function finalresult256 (H) 173 | -- Produce the final hash value (big-endian): 174 | return 175 | str2hexa(num2s(H[1], 4)..num2s(H[2], 4)..num2s(H[3], 4)..num2s(H[4], 4).. 176 | num2s(H[5], 4)..num2s(H[6], 4)..num2s(H[7], 4)..num2s(H[8], 4)) 177 | end 178 | 179 | 180 | ---------------------------------------------------------------------- 181 | local HH = {} -- to reuse 182 | 183 | local function hash224 (msg) 184 | msg = preproc(msg, #msg) 185 | local H = initH224(HH) 186 | 187 | -- Process the message in successive 512-bit (64 bytes) chunks: 188 | for i = 1, #msg, 64 do 189 | digestblock(msg, i, H) 190 | end 191 | 192 | return finalresult224(H) 193 | end 194 | 195 | 196 | local function hash256 (msg) 197 | msg = preproc(msg, #msg) 198 | local H = initH256(HH) 199 | 200 | -- Process the message in successive 512-bit (64 bytes) chunks: 201 | for i = 1, #msg, 64 do 202 | digestblock(msg, i, H) 203 | end 204 | 205 | return finalresult256(H) 206 | end 207 | ---------------------------------------------------------------------- 208 | local mt = {} 209 | 210 | local function new256 () 211 | local o = {H = initH256({}), msg = "", len = 0} 212 | setmetatable(o, mt) 213 | return o 214 | end 215 | 216 | mt.__index = mt 217 | 218 | function mt:add (m) 219 | self.msg = self.msg .. m 220 | self.len = self.len + #m 221 | local t = 0 222 | while #self.msg - t >= 64 do 223 | digestblock(self.msg, t + 1, self.H) 224 | t = t + 64 225 | end 226 | self.msg = self.msg:sub(t + 1, -1) 227 | end 228 | 229 | 230 | function mt:close () 231 | self.msg = preproc(self.msg, self.len) 232 | self:add("") 233 | return finalresult256(self.H) 234 | end 235 | ---------------------------------------------------------------------- 236 | 237 | return { 238 | hash224 = hash224, 239 | hash256 = hash256, 240 | new256 = new256, 241 | } 242 | -------------------------------------------------------------------------------- /spec/env/time.lua: -------------------------------------------------------------------------------- 1 | local time = {} 2 | 3 | function time.new(spec) 4 | local instance = {} 5 | 6 | instance.seconds = function() 7 | local now = spec.now or 0 -- we could use os.time() instead of 0 here; debugging is easier though 8 | spec.now = now 9 | return now 10 | end 11 | 12 | instance.now = instance.seconds 13 | 14 | instance.http = function(when) 15 | when = when or instance.seconds() 16 | -- Thu, 18 Nov 2010 11:27:35 GMT 17 | -- %e returns extra spaces for 1-digit days; remove them with gsub 18 | return (os.date("%a, %e %h %Y %H:%M:%S GMT", when):gsub(" ", " ")) 19 | end 20 | 21 | return instance 22 | end 23 | 24 | return time 25 | -------------------------------------------------------------------------------- /spec/sandbox.lua: -------------------------------------------------------------------------------- 1 | local sandbox = { 2 | _VERSION = "sandbox 0.5", 3 | _DESCRIPTION = "A pure-lua solution for running untrusted Lua code.", 4 | _URL = "https://github.com/kikito/sandbox.lua", 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | -- The base environment is merged with the given env option (or an empty table, if no env provided) 32 | -- 33 | local BASE_ENV = {} 34 | 35 | -- List of non-safe packages/functions: 36 | -- 37 | -- * string.rep: can be used to allocate millions of bytes in 1 operation 38 | -- * {set|get}metatable: can be used to modify the metatable of global objects (strings, integers) 39 | -- * collectgarbage: can affect performance of other systems 40 | -- * dofile: can access the server filesystem 41 | -- * _G: It has access to everything. It can be mocked to other things though. 42 | -- * load{file|string}: All unsafe because they can grant acces to global env 43 | -- * raw{get|set|equal}: Potentially unsafe 44 | -- * module|require|module: Can modify the host settings 45 | -- * string.dump: Can display confidential server info (implementation of functions) 46 | -- * string.rep: Can allocate millions of bytes in one go 47 | -- * math.randomseed: Can affect the host sytem 48 | -- * io.*, os.*: Most stuff there is non-save 49 | 50 | 51 | -- Safe packages/functions below 52 | ([[ 53 | 54 | _VERSION assert error ipairs next pairs 55 | pcall select tonumber tostring type unpack xpcall 56 | 57 | coroutine.create coroutine.resume coroutine.running coroutine.status 58 | coroutine.wrap coroutine.yield 59 | 60 | math.abs math.acos math.asin math.atan math.atan2 math.ceil 61 | math.cos math.cosh math.deg math.exp math.fmod math.floor 62 | math.frexp math.huge math.ldexp math.log math.log10 math.max 63 | math.min math.modf math.pi math.pow math.rad math.random 64 | math.sin math.sinh math.sqrt math.tan math.tanh 65 | 66 | os.clock os.difftime os.time 67 | 68 | string.byte string.char string.find string.format string.gmatch 69 | string.gsub string.len string.lower string.match string.reverse 70 | string.sub string.upper 71 | 72 | table.insert table.maxn table.remove table.sort 73 | 74 | ]]):gsub('%S+', function(id) 75 | local module, method = id:match('([^%.]+)%.([^%.]+)') 76 | if module then 77 | BASE_ENV[module] = BASE_ENV[module] or {} 78 | BASE_ENV[module][method] = _G[module][method] 79 | else 80 | BASE_ENV[id] = _G[id] 81 | end 82 | end) 83 | 84 | local function protect_module(module, module_name) 85 | return setmetatable({}, { 86 | __index = module, 87 | __newindex = function(_, attr_name, _) 88 | error('Can not modify ' .. module_name .. '.' .. attr_name .. '. Protected by the sandbox.') 89 | end 90 | }) 91 | end 92 | 93 | ('coroutine math os string table'):gsub('%S+', function(module_name) 94 | BASE_ENV[module_name] = protect_module(BASE_ENV[module_name], module_name) 95 | end) 96 | 97 | -- auxiliary functions/variables 98 | 99 | local function merge(dest, source) 100 | for k,v in pairs(source) do 101 | dest[k] = dest[k] or v 102 | end 103 | return dest 104 | end 105 | 106 | local function sethook(f, key, quota) 107 | if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end 108 | debug.sethook(f, key, quota) 109 | end 110 | 111 | -- Public interface: sandbox.protect 112 | function sandbox.protect(f, options) 113 | if type(f) == 'string' then f = assert(loadstring(f)) end 114 | 115 | options = options or {} 116 | 117 | local quota = false 118 | if options.quota ~= false then 119 | quota = options.quota or 500000 120 | end 121 | 122 | local env = merge(options.env or {}, BASE_ENV) 123 | env._G = env._G or env 124 | 125 | setfenv(f, env) 126 | 127 | return function(...) 128 | 129 | if quota then 130 | local timeout = function() 131 | sethook() 132 | error('Quota exceeded: ' .. tostring(quota)) 133 | end 134 | sethook(timeout, "", quota) 135 | end 136 | 137 | local ok, result = pcall(f, ...) 138 | 139 | sethook() 140 | 141 | if not ok then error(result) end 142 | return result 143 | end 144 | end 145 | 146 | -- Public interface: sandbox.run 147 | function sandbox.run(f, options, ...) 148 | return sandbox.protect(f, options)(...) 149 | end 150 | 151 | -- make sandbox(f) == sandbox.protect(f) 152 | setmetatable(sandbox, {__call = function(_,f,o) return sandbox.protect(f,o) end}) 153 | 154 | return sandbox 155 | -------------------------------------------------------------------------------- /spec/spec.lua: -------------------------------------------------------------------------------- 1 | require 'spec.assert_contains' 2 | require 'spec.assert_difference' 3 | 4 | local spy = require 'luassert.spy' 5 | 6 | local env = require 'spec.env.env' 7 | local sandbox = require 'spec.sandbox' 8 | local http_utils = require 'spec.env.http_utils' 9 | 10 | local spec = {} 11 | 12 | ------------------------------ 13 | spec.request = function(req) 14 | return http_utils.complete_request(req) 15 | end 16 | 17 | spec.next_middleware = function(f) 18 | return spy.new(function() 19 | local start = spec.time.now() 20 | local response = http_utils.complete_response(f()) 21 | spec.trace.time = spec.time.now() - start 22 | return response 23 | end) 24 | end 25 | 26 | spec.middleware = function(path) 27 | for k,v in pairs(spec) do 28 | if type(v) ~= 'function' then spec[k] = nil end 29 | end 30 | 31 | local environment = env.new(spec) 32 | 33 | path = 'middleware/' .. path 34 | 35 | local loaded_f = assert(loadfile(path)) 36 | local sandboxed_f = sandbox.protect(loaded_f, {env = environment}) 37 | return sandboxed_f() 38 | end 39 | 40 | spec.mock_http = function(request, response) 41 | local mocks = spec.http_mocks or {} 42 | spec.http_mocks = mocks 43 | 44 | mocks[#mocks + 1] = { 45 | request = http_utils.complete_request(request), 46 | response = http_utils.complete_response(response) 47 | } 48 | end 49 | 50 | spec.advance_time = function(seconds) 51 | spec.now = spec.now or spec.time.seconds() 52 | spec.now = spec.now + seconds 53 | end 54 | 55 | return spec 56 | --------------------------------------------------------------------------------