├── LICENCE ├── README.md ├── data ├── architecture_diagram.png └── architecture_diagram_ascii_art.txt ├── lb ├── .gitignore ├── conf │ └── nginx.conf ├── html │ └── demo │ │ ├── method_ajax_async.js │ │ ├── method_api_aggregation.js │ │ ├── no_cache.js │ │ └── test_api_aggr.html └── logs │ └── .gitignore └── sandbox ├── .gitignore ├── conf └── nginx.conf ├── logs └── .gitignore └── lua ├── system ├── sandbox.lua └── utils_3scale.lua └── user_scripts ├── bar.lua ├── foo.lua └── positive_word.lua /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 3scale networks S.L. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## API AGGREGATOR 3 | 4 | API Aggregator is a system that combines lua and nginx to have a sandbox environment where you can safely run user generated scripts that do API aggregation. 5 | 6 | **Why API aggregation?** 7 | 8 | REST API's are chatty because of its fine granularity. The high number of requests needed to accomplish non-trivial use cases can affect the performance of applications using such API's. This problem is particularly acute for mobile apps. See the [blog post](http://3scale.github.io/2013/04/18/accelerate-your-mobile-api-with-nginx-and-lua/) for some empirical results, for that particular case, requests time was reduced by a factor or 3. 9 | 10 | **How?** 11 | 12 | Instead of accessing the public methods of the API, a developer can create a lua script that has the full workflow for their use-case. This lua script can be run safely on the servers of the API provider (if they use API Aggregator, that is). 13 | 14 | The underlying idea is pretty much like **stored procedures for APIs**. 15 | 16 | Note that this approach is not unheard of. [Netflix](http://netflix.com), for instance, has a [JVM-based sandbox environment](http://techblog.netflix.com/2013/01/optimizing-netflix-api.html) so that their different development teams can create custom end-points for their specific needs on top of REST based API. With API Aggregator you can get quite close to the same design :-) 17 | 18 | 19 | ## ADDING API AGGREGATION SCRIPTS 20 | 21 | To add a user generated script ("the stored procedure") you only need to drop the lua file to the directory defined in $lua_user_scripts_path. 22 | 23 | After adding the script, the end-point will be dynamically generated in runtime and it will be immediately available. 24 | 25 | The scripts must be unnamed functions, 26 | 27 | ```lua 28 | return function() 29 | -- magic goes here 30 | ngx.exit(ngx.HTTP_OK) 31 | end 32 | ``` 33 | 34 | A example of a real user script, 35 | 36 | ```lua 37 | return function() 38 | 39 | local max_sentiment = 5 40 | local params = ngx.req.get_query_args() 41 | local path = utils.split(ngx.var.request," ")[2] 42 | 43 | -- Get the sentence to be analyzed from the URL path 44 | local sentence = ngx.re.match(path,[=[^/aggr/positive_word/(.+).json]=])[1] 45 | sentence = utils.unescape(sentence) 46 | 47 | -- Do the REST API request to get the sentiment value of the sentence 48 | local res_sentence = ngx.location.capture("/v1/sentence/".. utils.escape(sentence) .. ".json" ) 49 | local result = cjson.decode(res_sentence.body) 50 | 51 | -- If positive 52 | if (result["sentiment"]>0) then 53 | 54 | sentence = utils.unescape(sentence) 55 | 56 | local max = nil 57 | local words = utils.split(sentence," ") 58 | 59 | -- for each word in the sentence, do the REST API request to get the sentiment value of the 60 | -- word 61 | for i,w in pairs(words) do 62 | local res_word = ngx.location.capture("/v1/word/".. utils.escape(w) .. ".json" ) 63 | local word = cjson.decode(res_word.body) 64 | if max == nil or max < word.sentiment then 65 | max = word.sentiment 66 | result.highest_positive_sentiment_word = word 67 | if word.sentiment == max_sentiment then 68 | break 69 | end 70 | end 71 | end 72 | end 73 | 74 | ngx.header.content_type = "application/json" 75 | ngx.say(cjson.encode(result)) 76 | ngx.exit(ngx.HTTP_OK) 77 | 78 | end 79 | ``` 80 | 81 | 82 | This lua script aggregates 1+N requests to a REST API to serve a very particular use-case: to get the word with a highest positive emotional value of a sentence if it's positive. This use case is very specific to a particular application, so it should not be "public". 83 | 84 | However, being able to colocate the aggregator script with the API have multiple benefits for all parties involved: 85 | 86 | * For API consumers: it reduces the number of requests, thus reducing the page load time and if it's a mobile app, power consumption. Furthermore, it eliminates the need to run a backend services for your API to do exactly the type of aggregation that can be done now 87 | by the provider using API Aggregator. 88 | * For API providers, the bandwidth and the number of open connections is reduced. And what's more important, you are making your API very friendly 89 | to developers to use since they can create custom scripts that meet their particular use-cases. 90 | 91 | 92 | After adding the lua script the developer can immediately access a new API endpoint named _/aggr/positive_word/*.json__. This new API method does all the heavy-lifting against the REST API of the provider. 93 | 94 | The end-point is derived from the name of the lua file, or it can be customized on the configuration file. 95 | 96 | ## ARCHITECTURE 97 | 98 | The diagram depicts the flow of the example that you can setup locally if you follow the HowTo Install section. 99 | 100 | ![Architecture Diagram](/data/architecture_diagram.png "Architecture Diagram") 101 | 102 | 103 | ## HOWTO INSTALL 104 | 105 | ### 1) Install Nginx with Lua Support 106 | 107 | You can add the lua module to your nginx or you can use the bundle OpenResty that has nginx with lua 108 | (and other great extensions) already built-in. 109 | 110 | For Debian/Ubuntu linux distribution you should install the following packages using apt-get: 111 | 112 | sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev perl 113 | 114 | For different systems check out the [OpenResty](http://openresty.org/#Installation) documentation. 115 | 116 | Change VERSION with your desired version (we tested 1.2.3.8) 117 | 118 | wget http://agentzh.org/misc/nginx/ngx_openresty-VERSION.tar.gz 119 | tar -zxvf ngx_openresty-VERSION.tar.gz 120 | cd ngx_openresty-VERSION/ 121 | 122 | ./configure --prefix=/opt/openresty --with-luajit --with-http_iconv_module -j2 123 | 124 | make 125 | make install 126 | 127 | You can change the --prefix to set up your base directory. 128 | 129 | 130 | ### 2) Start the Nginx servers 131 | 132 | **For the load balancer** (not needed in production) 133 | 134 | You can start the Nginx that acts as load balancer (and that hosts the html5 app demo) like this: 135 | 136 | /opt/openresty/nginx/sbin/nginx -p `pwd`/lb/ 137 | 138 | This assumes you are on that in the base directory of the api-aggregation project. You can always replace 139 | the `pwd` with your full path to the _api-aggregator/lb_ directory. 140 | 141 | Note that the nginx load balancer is not needed, you can use your own balancer. This is just included for convenience. 142 | 143 | The HTML5 App demo is included in the load balancer (_lb/html/demo_). Once you run the load balancer, you can access it 144 | at _localhost:8000/demo/_ 145 | 146 | To stop it. Just use the same line with `-s stop` 147 | 148 | /opt/openresty/nginx/sbin/nginx -p `pwd`/lb/ -s stop 149 | 150 | 151 | **For the sandbox** 152 | 153 | The sandbox is not optional :-) It's where the lua scripts that do the API aggregation run. 154 | 155 | Before running the sandbox you must change the variable $lua_user_scripts_path to the full path of the lua user scripts 156 | on the _sandbox/conf/nginx.conf_ 157 | 158 | set $lua_user_scripts_path "/path/to/api-aggregator/sandbox/lua/user_scripts/"; 159 | 160 | All the user lua scripts that do API aggregation (the stored procedures) should be in the directory defined above. 161 | 162 | The name of the lua script is used to build the API endpoint, the URL. For instance, a script called _positive_word.lua_ 163 | will automatically be available at the end-point /aggr/positive_word/*. If you want an end-point /aggr/foo/ID.xml you will 164 | have to create a lua script called foo.lua. This convention is used to automatically map user scripts to API end-points, you 165 | can force arbitrary mappings using the directive _location_ of nginx. 166 | 167 | Once you updated the _sandbox/conf/nginx.conf_ you can start it: 168 | 169 | /opt/openresty/nginx/sbin/nginx -p `pwd`/sandbox/ 170 | 171 | Again, you can stop it with `-s stop` 172 | 173 | The sandbox is running on _localhost:8090_ (unless you change the listen port on the config file _api-aggregator/sandbox/conf/nginx.conf_) 174 | 175 | ### 3) Setup the SentimentAPI 176 | 177 | If you want, you can use the SentimentAPI instead of your own API. Installing it's quite straight forward: 178 | 179 | git clone 180 | cd sentiment-api-example 181 | ruby ./sentiment-api.rb 8080 182 | 183 | SentimentAPI is running on `localhost:8080`. You can test it with: 184 | 185 | curl -g "http://localhost:8080/v1/word/awesome.json" 186 | 187 | 188 | ### 4) Ready to go 189 | 190 | Go to your browser to _localhost:8000/demo/_. You will get the HTML5 App demo that showcases the performance improvements 191 | of API aggregation over direct REST access. Enjoy! 192 | 193 | ## TROUBLESHOOTING 194 | 195 | It's quit advisable to keep an eye on the error.log when trying it out, 196 | 197 | tail -f */logs/error.log 198 | 199 | ## CONTRIBUTORS 200 | 201 | * Josep M. Pujol (solso) 202 | * Raimon Grau (kidd) 203 | 204 | ## LICENSE 205 | 206 | MIT License 207 | 208 | Copyright (c) 2013 3scale 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /data/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solso/api-aggregator/2ea1f6b38f9bd4465255cd3a60a6dd6a6442dd9b/data/architecture_diagram.png -------------------------------------------------------------------------------- /data/architecture_diagram_ascii_art.txt: -------------------------------------------------------------------------------- 1 | 2 | +-+ | 3 | | | : 4 | +-+ +------+ | 5 | |cBLU | | +---------------+ 6 | |+--------+| | | Nginx LB | 7 | ||cWHI ||GET /v1/sentence/SENTENCE.json (1) | | REST API Backend +-------------+ 8 | || |+---------------------------------------->| (or your | (1) routes /v1/ | API App | 9 | || || : | LB of choice | | +--+ 10 | || || | | e.g. Haproxy) +------------------->|(Thin Process| | 11 | || ||GET /aggr/positive_word/SENTENCE.json (2)| | |with Ruby for| +--+ 12 | || |+---------------------------------------->| | |SentimentAPI)| | | 13 | |+--------+| : | | +---+---------+ | | 14 | |MobileApp | | | | | | | 15 | +----------+ | | | +---+--------+ | 16 | | | | | | 17 | | | | +-----------+ 18 | | +--->| | API Aggregator 19 | | | | | (2) routes /aggr/ 20 | | | | +--------------+ 21 | | | | | | 22 | | | | | | 23 | | | +---------------+ | 24 | | | The .lua script does the | 25 | | | requests to the REST API. | 26 | | | Converts a single (2) call | 27 | | to multiple (1) calls v 28 | | +-------------------------------+ 29 | | |Nginx+LUA API Aggregator | 30 | | | /---------------------------\ | 31 | | | |Sandbox | | 32 | +----------------------------+ | /-----------------------\ | | 33 | | | |positive_word.lua {d} | | | 34 | | | \-----------------------/ | | 35 | | | ... | | 36 | | | /-----------------------\ | | 37 | | | |something_else.lua {d} | | | 38 | | | \-----------------------/ | | 39 | | | /-----------------------\ | | 40 | | | |another.lua {d} | | | 41 | | | \-----------------------/ | | 42 | | \---------------------------/ | 43 | +-------------------------------+ 44 | -------------------------------------------------------------------------------- /lb/.gitignore: -------------------------------------------------------------------------------- 1 | client_body_temp/** 2 | fastcgi_temp/** 3 | proxy_temp/** 4 | scgi_temp/** 5 | uwsgi_temp/** 6 | logs/** 7 | !logs/.gitignore 8 | -------------------------------------------------------------------------------- /lb/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | ## --- 2 | ## Mininal configuration to have Nginx+LUA (Openresty) to act as a load-balancer between your 3 | ## api backend and your api aggregation sandbox. This does not have to be nginx at all, your 4 | ## load balancer can do this very well 5 | ## --- 6 | 7 | worker_processes 2; 8 | error_log logs/error.log warn; 9 | events { worker_connections 256; } 10 | 11 | http { 12 | 13 | ## upstream of your api 14 | ## this one assumes you have the SentimentAPI (https://github.com/solso/sentiment-api-example) 15 | ## running on port 8080 16 | upstream api-backend { 17 | server localhost:8080 max_fails=5 fail_timeout=30; 18 | } 19 | 20 | ## upstream of your api aggregation sandbox, in this case is running on same server but you can 21 | ## put the sandbox anywhere you want, even outside your data-center 22 | upstream aggregation-sandbox { 23 | server localhost:8090 max_fails=5 fail_timeout=30; 24 | } 25 | 26 | server { 27 | listen 8000; 28 | server_name api-sentiment.3scale.net; 29 | 30 | ## this locations is only to serve the demo html5 app 31 | location ~/demo { 32 | ## CHANGEME, set the path to the html5 app demo 33 | root "html/"; 34 | index test_api_aggr.html; 35 | } 36 | 37 | ## if the request starts with /aggr, send it to the aggregation sandbox 38 | location ~ ^/aggr/ { 39 | proxy_pass http://aggregation-sandbox; 40 | } 41 | 42 | ## whatever is not /aggr (prefix for api aggregations) will be send to the api-backend, as it 43 | ## normally should :) 44 | location / { 45 | proxy_pass http://api-backend; 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lb/html/demo/method_ajax_async.js: -------------------------------------------------------------------------------- 1 | function method_ajax_async(sentence) { 2 | $.mobile.loading('show'); 3 | $("#submit-btn").button("disable"); 4 | 5 | var start = new Date(); 6 | var result = null; 7 | var current_max = Number.NEGATIVE_INFINITY; 8 | var max_sentiment = 5; 9 | var num_req = 0; 10 | var max_word = null; 11 | 12 | jQuery.ajax({ 13 | url: "/v1/sentence/" + escape(sentence) + ".json?" + vodafone_please_stop_caching_everything(), 14 | success: function(data) { 15 | result = data; 16 | if (result["sentiment"]>0) { 17 | var words = sentence.split(" "); 18 | for(var i=0;i3) { 20 | num_req++; 21 | jQuery.ajax({ 22 | url: "/v1/word/" + escape(words[i]) + ".json?" + vodafone_please_stop_caching_everything(), 23 | success: function(data) { 24 | current_word = data; 25 | num_req--; 26 | if (current_word!=null && current_word["sentiment"]>current_max) { 27 | current_max = current_word["sentiment"]; 28 | max_word = current_word; 29 | } 30 | if (num_req==0) { 31 | $.mobile.loading('hide'); 32 | $("#submit-btn").button("enable"); 33 | 34 | $("#results").prepend("
AJAX Async API requests: " + (new Date() - start) + " ms
" + JSON.stringify(max_word) + "
"); 35 | } 36 | }, 37 | error: function() { 38 | $.mobile.loading('hide'); 39 | $("#submit-btn").button("enable"); 40 | $("#results").prepend("
Woops! Please try again later.
"); 41 | }, 42 | async:true, 43 | dataType:"json" 44 | }); 45 | } 46 | } 47 | } 48 | else { 49 | $.mobile.loading('hide'); 50 | $("#submit-btn").button("enable"); 51 | $("#results").prepend("
AJAX Async API requests: " + (new Date() - start) + " ms
Sentence has no positive emotional value at all
"); 52 | } 53 | }, 54 | error: function() { 55 | $.mobile.loading('hide'); 56 | $("#submit-btn").button("enable"); 57 | $("#results").prepend("
Woops! Please try again later.
"); 58 | }, 59 | async:true, 60 | dataType:"json" 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /lb/html/demo/method_api_aggregation.js: -------------------------------------------------------------------------------- 1 | function method_aggregation(sentence) { 2 | $.mobile.loading('show'); 3 | $("#submit-btn").button("disable"); 4 | 5 | var start = new Date(); 6 | var result = null; 7 | 8 | jQuery.ajax({ 9 | url: "/aggr/positive_word/" + escape(sentence) + ".json?" + vodafone_please_stop_caching_everything(), 10 | success: function(data) { 11 | $.mobile.loading('hide'); 12 | $("#submit-btn").button("enable"); 13 | if (data["highest_positive_sentiment_word"]==null || data["highest_positive_sentiment_word"]==undefined) { 14 | $("#results").prepend("
AJAX Async API requests: " + (new Date() - start) + " ms
Sentence has no positive emotional value at all
"); 15 | } 16 | else { 17 | $("#results").prepend("
Aggregated API requests: " + (new Date() - start) + " ms
" + JSON.stringify(data["highest_positive_sentiment_word"]) + "
"); 18 | } 19 | }, 20 | error: function() { 21 | $.mobile.loading('hide'); 22 | $("#submit-btn").button("enable"); 23 | $("#results").prepend("
Woops! Please try again later.
"); 24 | }, 25 | async:true, 26 | dataType:"json" 27 | }); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /lb/html/demo/no_cache.js: -------------------------------------------------------------------------------- 1 | function vodafone_please_stop_caching_everything() { 2 | return "vpsce="+Math.random(); 3 | } -------------------------------------------------------------------------------- /lb/html/demo/test_api_aggr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 38 | 39 | 92 | 93 | 94 | 95 |
96 |
97 |

API Aggregation

98 |
99 |
100 | 101 |
102 | 103 | 104 | 105 | 106 | 107 | 111 | 112 |
113 |
114 | 115 |
116 |
117 |
118 |
119 |
120 |

What is demo about?

121 |

REST APIs are chatty and for a mobile environment they can become very slow. How slow? You can check it for yourself.

122 |

To known how you can speed up your API for mobile access go to the blog blog post.

123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /lb/logs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solso/api-aggregator/2ea1f6b38f9bd4465255cd3a60a6dd6a6442dd9b/lb/logs/.gitignore -------------------------------------------------------------------------------- /sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | client_body_temp/** 2 | fastcgi_temp/** 3 | proxy_temp/** 4 | scgi_temp/** 5 | uwsgi_temp/** 6 | logs/** 7 | !logs/.gitignore 8 | -------------------------------------------------------------------------------- /sandbox/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | ## --- 2 | ## Mininal configuration to have Nginx+LUA (Openresty) to acts as sandbox that run lua based api 3 | ## aggregation scripts 4 | ## --- 5 | 6 | worker_processes 2; 7 | error_log logs/error.log warn; 8 | events { worker_connections 256; } 9 | 10 | http { 11 | 12 | ## upstream of your api 13 | ## this one assumes you have the SentimentAPI (https://github.com/solso/sentiment-api-example) 14 | ## running on port 8080 15 | upstream api-backend { 16 | server localhost:8080 max_fails=5 fail_timeout=30; 17 | } 18 | 19 | server { 20 | ## the port of your sandboxed nginx 21 | listen 8090; 22 | server_name api-sentiment.3scale.net; 23 | 24 | ## for each api aggregation lua script you need a localtion, which will be the end-point used 25 | ## by the API consumers, e.g. GET /aggr/positive_word/SENTENCE.json 26 | location ~ ^/aggr/ { 27 | ## CHANGEME 28 | ## you must define full path to the user_scripts directory 29 | ## set $lua_user_scripts_path "/path/to/api-aggregator/sandbox/lua/user_scripts/"; 30 | set $lua_user_scripts_path "/Users/solso/3scale/api-aggregator/sandbox/lua/user_scripts/"; 31 | 32 | ## utils_sandbox will route the request to the proper lua script in user_scripts/ based on the url path 33 | ## after /aggr/ and before any of the following characters /&?. or end of string. 34 | ## for instance 35 | ## /aggr/positive_word/sentence.json maps to user_scripts/positive_word.lua 36 | ## /aggr/another_script.xml maps to user_scripts/another_script.lua 37 | ## /aggr/foo_bar maps to users_scripts/foo_bar.lua 38 | access_by_lua_file lua/system/sandbox.lua; 39 | } 40 | 41 | ## whatever is not /aggr (prefix for api aggregations) will be send to the api-backend 42 | location / { 43 | internal; 44 | proxy_pass http://api-backend; 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /sandbox/logs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solso/api-aggregator/2ea1f6b38f9bd4465255cd3a60a6dd6a6442dd9b/sandbox/logs/.gitignore -------------------------------------------------------------------------------- /sandbox/lua/system/sandbox.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Run the API aggregation scripts on a sandboxed environment 4 | 5 | In the table sandboxed_env you define all the functions that the api aggregation 6 | lua scripts will be able to use. For instance, we can limit the functions available 7 | on the nginx module: { var = ngx.var, re = ngx.re, location = ngx.location, 8 | header = ngx.header, exit = ngx.exit, say = ngx.say, HTTP_OK = ngx.HTTP_OK} 9 | 10 | You can be as restrictive as you want. Note that each request in nginx runs the lua 11 | script in a different lua virtual machine, so it's not possible to cross-contaminate 12 | requests. 13 | 14 | Check http://lua-users.org/wiki/SandBoxes for more pointers 15 | 16 | Additionally, you can set up a timeout of the script by setting a value on, 17 | 18 | debug.sethook(timeout_response, "", 5000) 19 | 20 | The 5000 is the number of operations that the lua script can execute before timing 21 | out. This will kill automatically any long running requests. Tuning this parameters 22 | depends on the complexity of the scripts that you allow to run. 5000 is plenty for 23 | the sentence_with_highest_word.lua 24 | 25 | ]]-- 26 | 27 | sandboxed_env = { 28 | ipairs = ipairs, 29 | next = next, 30 | pairs = pairs, 31 | pcall = pcall, 32 | tonumber = tonumber, 33 | tostring = tostring, 34 | type = type, 35 | unpack = unpack, 36 | string = { byte = string.byte, char = string.char, find = string.find, 37 | format = string.format, gmatch = string.gmatch, gsub = string.gsub, 38 | len = string.len, lower = string.lower, match = string.match, 39 | rep = string.rep, reverse = string.reverse, sub = string.sub, 40 | upper = string.upper }, 41 | table = { insert = table.insert, maxn = table.maxn, remove = table.remove, 42 | sort = table.sort }, 43 | math = { abs = math.abs, acos = math.acos, asin = math.asin, 44 | atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos, 45 | cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor, 46 | fmod = math.fmod, frexp = math.frexp, huge = math.huge, 47 | ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max, 48 | min = math.min, modf = math.modf, pi = math.pi, pow = math.pow, 49 | rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh, 50 | sqrt = math.sqrt, tan = math.tan, tanh = math.tanh }, 51 | os = { clock = os.clock, difftime = os.difftime, time = os.time }, 52 | print = print, 53 | utils = require "utils_3scale", 54 | cjson = require "cjson", 55 | ngx = ngx 56 | } 57 | 58 | --[[ 59 | Needs to file which file to lua based on the URL 60 | ]]-- 61 | 62 | local utils = require "utils_3scale" 63 | local path = utils.split(ngx.var.request," ")[2] 64 | local user_script_file = ngx.re.match(path,[=[^\/aggr\/([a-zA-Z0-9-_]+)]=])[1] 65 | lc = loadfile(ngx.var.lua_user_scripts_path..user_script_file..".lua") 66 | 67 | if (lc == nil) then 68 | ngx.exit(ngx.HTTP_NOT_FOUND) 69 | else 70 | user_function = lc() 71 | if (user_function == nil) then 72 | ngx.exit(ngx.HTTP_NOT_FOUND) 73 | end 74 | end 75 | 76 | local timeout_response = function() 77 | debug.sethook() 78 | error("The lua script " .. ngx.var.function_to_call_file .. " has timed out!!") 79 | end 80 | 81 | debug.sethook(timeout_response, "", 5000) 82 | 83 | setfenv(user_function, sandboxed_env) 84 | local res_user_function = pcall(user_function) 85 | if not res_user_function then 86 | error("There was an error running " .. ngx.var.function_to_call_file) 87 | end 88 | debug.sethook() 89 | 90 | -------------------------------------------------------------------------------- /sandbox/lua/system/utils_3scale.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Auxiliary functions 3 | ]]-- 4 | 5 | local M = {} -- public interface 6 | 7 | function M.split(self, delimiter) 8 | local result = { } 9 | local from = 1 10 | local delim_from, delim_to = string.find( self, delimiter, from ) 11 | while delim_from do 12 | table.insert( result, string.sub( self, from , delim_from-1 ) ) 13 | from = delim_to + 1 14 | delim_from, delim_to = string.find( self, delimiter, from ) 15 | end 16 | table.insert( result, string.sub( self, from ) ) 17 | return result 18 | end 19 | 20 | function M.escape(self) 21 | local nstr = string.gsub(self, "([&=+%c])", function (c) return string.format("%%%02X", string.byte(c)) end) 22 | return string.gsub(nstr, " ", "+") 23 | end 24 | 25 | function M.unescape(self) 26 | local nstr = string.gsub(self, "+", " ") 27 | return string.gsub(nstr, "%%(%x%x)", function (h) return string.char(tonumber(h, 16)) end) 28 | end 29 | 30 | function M.get_query_args_extended(self) 31 | local args = self 32 | for k, v in pairs(args) do 33 | t = {} 34 | for k2 in string.gmatch(k, "%b[]") do 35 | table.insert(t,string.sub(k2,2,string.len(k2)-1)) 36 | end 37 | if #t > 0 then 38 | -- it has nested params, needs to be transformed 39 | first = string.sub(k,1,string.find(k,"%[")-1) 40 | if args[first]==nil or type(args[first])~="table" then 41 | args[first] = {} 42 | end 43 | args[first][t[1]] = v 44 | end 45 | end 46 | return args 47 | end 48 | 49 | return M 50 | -------------------------------------------------------------------------------- /sandbox/lua/user_scripts/bar.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Example of a user script 3 | ]]-- 4 | 5 | return function() 6 | local path = utils.split(ngx.var.request," ")[2] 7 | ngx.header.content_type = "text/plain" 8 | ngx.say(path.." rules!") 9 | ngx.exit(ngx.HTTP_OK) 10 | end -------------------------------------------------------------------------------- /sandbox/lua/user_scripts/foo.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Example of a user script 3 | ]]-- 4 | 5 | return function() 6 | local path = utils.split(ngx.var.request," ")[2] 7 | ngx.header.content_type = "text/plain" 8 | ngx.say(path.." rules!") 9 | ngx.exit(ngx.HTTP_OK) 10 | end -------------------------------------------------------------------------------- /sandbox/lua/user_scripts/positive_word.lua: -------------------------------------------------------------------------------- 1 | 2 | --[[ 3 | User script that returns the word with highest positive emotional value. 4 | It runs against the SentimentAPI REST AP. See blog entry for the full details. 5 | ]]-- 6 | 7 | return function() 8 | 9 | local max_sentiment = 5 10 | local params = ngx.req.get_query_args() 11 | local path = utils.split(ngx.var.request," ")[2] 12 | 13 | -- Get the sentence to be analyzed from the URL path 14 | local sentence = ngx.re.match(path,[=[^/aggr/positive_word/(.+).json]=])[1] 15 | sentence = utils.unescape(sentence) 16 | 17 | -- Do the REST API request to get the sentiment value of the sentence 18 | local res_sentence = ngx.location.capture("/v1/sentence/".. utils.escape(sentence) .. ".json" ) 19 | local result = cjson.decode(res_sentence.body) 20 | 21 | -- If positive 22 | if (result["sentiment"]>0) then 23 | 24 | sentence = utils.unescape(sentence) 25 | 26 | local max = nil 27 | local words = utils.split(sentence," ") 28 | 29 | -- for each word in the sentence, do the REST API request to get the sentiment value of the 30 | -- word 31 | for i,w in pairs(words) do 32 | local res_word = ngx.location.capture("/v1/word/".. utils.escape(w) .. ".json" ) 33 | local word = cjson.decode(res_word.body) 34 | if max == nil or max < word.sentiment then 35 | max = word.sentiment 36 | result.highest_positive_sentiment_word = word 37 | if word.sentiment == max_sentiment then 38 | break 39 | end 40 | end 41 | end 42 | end 43 | 44 | ngx.header.content_type = "application/json" 45 | ngx.say(cjson.encode(result)) 46 | ngx.exit(ngx.HTTP_OK) 47 | 48 | end 49 | --------------------------------------------------------------------------------