├── jack_hammerton.jpg ├── config ├── dev.json ├── test.json └── index.js ├── test.sh ├── opendsp.supervisor.conf ├── lib ├── middleware │ ├── clogger.js │ ├── empty-bid.js │ ├── lag.js │ └── bid.js ├── dsp.js └── router.js ├── index.js ├── Gruntfile.js ├── .gitignore ├── package.json ├── README.md ├── LICENSE └── opendsp.nginx.conf /jack_hammerton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avocarrot/opendsp/HEAD/jack_hammerton.jpg -------------------------------------------------------------------------------- /config/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "lag": 400, 4 | "port": 3000, 5 | "timeout": 200 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "lag": 10, 4 | "port": 4001, 5 | "timeout": 50000, 6 | "empty_bid": 0.00001 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | NODE_ENV=test node ./index.js & 4 | node_pid=$! 5 | trap "kill $node_pid" INT QUIT TERM EXIT 6 | sleep 1 7 | bid_id=$(curl -s -H 'Content-Type: application/json' -d '{ "id": "foo", "imp": [] }' localhost:4001/bid | jq -r .seatbid[].bid[].id) 8 | test x$bid_id = x123123214 9 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var nconf = require('nconf'); 2 | var util = require('util'); 3 | 4 | var env = (process.env.NODE_ENV || 'dev'); 5 | var file = util.format('%s/%s.json', __dirname, env); 6 | 7 | nconf.argv() 8 | .env() 9 | .file({ file: file }) 10 | .load(); 11 | 12 | module.exports = nconf; 13 | -------------------------------------------------------------------------------- /opendsp.supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:opendsp] 2 | directory=/srv/www 3 | user=nodejs 4 | autostart=true 5 | autorestart=true 6 | environment=NODE_ENV=dev 7 | command=node index.js --process_num %(process_num)02d --numprocs 2 --port 30%(process_num)02d 8 | process_name=%(program_name)s_%(process_num)02d 9 | numprocs=4 10 | -------------------------------------------------------------------------------- /lib/middleware/clogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Console Logger middleware. 3 | * 4 | * @return {GeneratorFunction} 5 | * @api public 6 | */ 7 | module.exports = function() { 8 | return function *(next){ 9 | var start = new Date; 10 | yield next; 11 | var ms = new Date - start; 12 | console.log('%s %s - %s ms', this.method, this.url, ms); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var nconf = require('./config'); 2 | var DSP = require('./lib/dsp'); 3 | 4 | var server = new DSP({ 5 | lag: (nconf.get('server:lag') || 0), 6 | details: (nconf.get('dsp') || { name: 'jack-hammerton' }), 7 | timeout: (nconf.get('server:timeout') || 0), 8 | empty_bid: (nconf.get('server:empty_bid') || 0) 9 | }); 10 | 11 | server.listen((nconf.get('port') || nconf.get('server:port') || 3000)); 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | nodemon: { 4 | dev: { 5 | script: 'index.js', 6 | options: { 7 | env: { 8 | ENV: 'dev' 9 | } 10 | } 11 | }, 12 | watch: [ 'lib/**/*.js', 'index.js' ] 13 | } 14 | }); 15 | grunt.loadNpmTasks('grunt-nodemon'); 16 | 17 | grunt.registerTask('default', [ 'nodemon:dev' ]); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/middleware/empty-bid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Empty bid middleware. 3 | * 4 | * Return an empty response with probability `prob`. 5 | * E.g: if `prob` is 0.9 (90%), then about 9 out of 10 6 | * requests will return an empty body and only 1 in 10 7 | * will return a bid. 8 | * 9 | * @param {double} prob 10 | * a number in the range [0, 1] 11 | */ 12 | module.exports = function(prob) { 13 | return function *(next) { 14 | if (prob > Math.random()) { 15 | this.response.status = 204; 16 | } 17 | 18 | yield next; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /lib/middleware/lag.js: -------------------------------------------------------------------------------- 1 | function timeoutAsync(ms, callback) { 2 | setTimeout(function () { 3 | callback(null, {}); 4 | }, Math.round(Math.random()*ms)); 5 | }; 6 | 7 | function timeoutThunk(ms) { 8 | return function(callback) { 9 | timeoutAsync(ms, callback); 10 | } 11 | }; 12 | /** 13 | * Lag middleware. 14 | * 15 | * @param {Integer} ms 16 | * @return {GeneratorFunction} 17 | * @api public 18 | */ 19 | module.exports = function(ms) { 20 | ms = (ms || 0); 21 | return function *(next) { 22 | if ( ms > 0) { 23 | yield timeoutThunk(ms); 24 | } 25 | yield next; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opendsp", 3 | "version": "0.0.1", 4 | "description": "HTTP Service implementation of a DSP in Node (v4.2.1)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=dev node index.js --port 3000", 8 | "test": "./test.sh" 9 | }, 10 | "engines": { 11 | "node": "4.2.1" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "dependencies": { 16 | "koa": "^1.1.0", 17 | "koa-json": "^1.1.1", 18 | "koa-json-body": "^4.0.0", 19 | "koa-router": "^5.2.3", 20 | "koa-timeout": "^0.1.1", 21 | "nconf": "^0.8.2", 22 | "openrtb": "avocarrot/openrtb" 23 | }, 24 | "devDependencies": { 25 | "grunt": "^0.4.5", 26 | "grunt-nodemon": "^0.4.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenDSP 2 | ======= 3 | HTTP Service implementation of a DSP in Node (v4.2.1). 4 | 5 | ![Jack Hammerton](https://raw.githubusercontent.com/Avocarrot/opendsp/master/jack_hammerton.jpg) 6 | >E. H. Sothern as Jack Hammerton in "The Highest Bidder" (1887). 7 | 8 | ##About 9 | 10 | [OpenDSP](https://github.com/Avocarrot/opendsp) is as a mock server for testing purposes. 11 | 12 | ##Bootstrap 13 | 14 | ``` 15 | # Get a local copy of opendsp. 16 | $ git clone https://github.com/Avocarrot/opendsp.git 17 | $ cd opendsp 18 | 19 | # You will need Node v4.2.1 for the `DSPMock`. In case you use `nvm`: 20 | $ nvm use v4.2.1 21 | 22 | # Npm install and start Grunt 23 | $ npm install 24 | $ grunt nodemon:dev 25 | ``` 26 | 27 | ##References 28 | - [Avoccarot Bid Request Specification](http://docs.avocarrot.com/avx/bid-request) 29 | - [Avoccarot Bid Response Specification](http://docs.avocarrot.com/avx/bid-response) 30 | -------------------------------------------------------------------------------- /lib/dsp.js: -------------------------------------------------------------------------------- 1 | var json_res = require('koa-json'); 2 | var json_req = require('koa-json-body'); 3 | 4 | var koa = require('koa'); 5 | 6 | var lag = require('./middleware/lag'); 7 | var logger = require('./middleware/clogger'); 8 | var empty_bid = require('./middleware/empty-bid'); 9 | var router = require('./router'); 10 | var timeout = require('koa-timeout'); 11 | 12 | module.exports = function DSP(config) { 13 | var server = koa(); 14 | 15 | server.context.dsp = config.details; 16 | 17 | if ( config.timeout ) { 18 | server.use(timeout(config.timeout)); 19 | } 20 | 21 | // logger 22 | server.use(logger()); 23 | server.use(json_req()); 24 | 25 | if ( config.lag ) { 26 | server.use(lag(config.lag)); 27 | } 28 | 29 | if ( config.empty_bid ) { 30 | server.use(empty_bid(config.empty_bid)); 31 | } 32 | 33 | server.use(json_res()); 34 | server.use(router.routes()) 35 | 36 | return server; 37 | }; 38 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | var router = require('koa-router')(); 2 | var bid = require('./middleware/bid'); 3 | 4 | router.post('/bid', bid.request, function *(next) { 5 | this.bids = [ { 6 | "id": "123123214", 7 | "impid": "31243341", 8 | "price": 5.80, 9 | "nurl": "http://example.com/track/win", 10 | "adomain": ["ford.com"], 11 | "iurl": "http://example.com/assets/image.png", 12 | "cid": "xxsw655xas", 13 | "cat": ["IAB1-4", "IAB1-5"], 14 | "adm": "{\"native\":{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is a sample ad from%s\"}},{\"id\":1,\"img\":{\"url\":\"http://cdn.exampleimage.com/2639042\",\"w\":100,\"h\":100}},{\"id\":2,\"img\":{\"url\":\"http://cdn.exampleimage.com/a/50/50/2639042 \",\"w\":50,\"h\":50}},{\"id\":3,\"data\":{\"value\":\"This is the ad description\"}},{\"id\":4,\"data\":{\"value\":4.5}},{\"id\":5,\"data\":{\"value\":\" Install now\"}}],\"link\":{\"url\":\"http://trackclick.com/Click\"},\"imptrackers\":[\"http://example.com/tracker\"]}}" 15 | } ]; 16 | yield next; 17 | }, bid.response); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Avocarrot 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/middleware/bid.js: -------------------------------------------------------------------------------- 1 | var openrtb = require('openrtb'); 2 | 3 | var request_builder = openrtb.getBuilder({ 4 | builderType: 'bidRequest' 5 | }); 6 | var response_builder = openrtb.getBuilder({ 7 | builderType: 'bidResponse' 8 | }); 9 | 10 | var util = require('util'); 11 | 12 | function *bid_request(next) { 13 | try { 14 | var bid_request = request_builder 15 | .id(this.request.body.id) 16 | .imp(this.request.body.imp) 17 | .build(); 18 | 19 | this.bid = { req: bid_request }; 20 | yield next; 21 | } catch(err) { 22 | this.response.body = { error: err.message }; 23 | this.response.status = 400; 24 | } 25 | }; 26 | 27 | function *bid_response(next) { 28 | if ( !this.bids || this.bids.length === 0 ) { 29 | this.response.status = 204; 30 | yield next; 31 | return; 32 | } 33 | 34 | try { 35 | var bid_response = response_builder 36 | .id(this.bid.req.id) 37 | .bidderName(this.dsp.name) 38 | .seatbid([ { bid: this.bids }]) 39 | .build(); 40 | 41 | bid_response.bidId = util.format('%s-%s', this.bid.req.id, this.dsp.name); 42 | this.response.body = bid_response; 43 | yield next; 44 | } catch(err) { 45 | this.response.body = { error: err.message }; 46 | this.response.status = 500; 47 | } 48 | }; 49 | 50 | module.exports = { 51 | request: bid_request, 52 | response: bid_response 53 | }; 54 | -------------------------------------------------------------------------------- /opendsp.nginx.conf: -------------------------------------------------------------------------------- 1 | # Define your "upstream" servers - the 2 | # servers request will be sent to 3 | upstream opendsp { 4 | least_conn; # Use Least Connections strategy 5 | server 127.0.0.1:3000; # NodeJS Server 1 6 | server 127.0.0.1:3001; # NodeJS Server 2 7 | server 127.0.0.1:3002; # NodeJS Server 1 8 | server 127.0.0.1:3003; # NodeJS Server 2 9 | } 10 | # Define the Nginx server 11 | # This will proxy any non-static directory 12 | server { 13 | listen 80; 14 | server_name dev.opendsp.com; 15 | 16 | access_log /var/log/nginx/dev.pendsp.com-access.log; 17 | error_log /var/log/nginx/dev.pendsp.com-error.log error; 18 | 19 | # Browser and robot always look for these 20 | # Turn off logging for them 21 | location = /favicon.ico { log_not_found off; access_log off; } 22 | location = /robots.txt { log_not_found off; access_log off; } 23 | 24 | # Handle static files so they are not proxied to NodeJS 25 | # You may want to also hand these requests to other upstream 26 | # servers, as you can define more than one! 27 | location ~ ^/(images/|img/|javascript/|js/|css/|stylesheets/|flash/|media/|static/|robots.txt|humans.txt|favicon.ico) { 28 | root /srv/www; 29 | } 30 | 31 | # pass the request to the node.js server 32 | # with some correct headers for proxy-awareness 33 | location / { 34 | proxy_set_header X-Real-IP $remote_addr; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header Host $http_host; 37 | proxy_set_header X-NginX-Proxy true; 38 | 39 | proxy_pass http://opendsp/; 40 | proxy_redirect off; 41 | 42 | # Handle Web Socket connections 43 | proxy_http_version 1.1; 44 | proxy_set_header Upgrade $http_upgrade; 45 | proxy_set_header Connection "upgrade"; 46 | } 47 | } 48 | 49 | server { 50 | listen 8080; 51 | server_name dev.opendsp.com; 52 | 53 | access_log /var/log/nginx/dev.pendsp.com-access.log; 54 | error_log /var/log/nginx/dev.pendsp.com-error.log error; 55 | 56 | # Browser and robot always look for these 57 | # Turn off logging for them 58 | location = /favicon.ico { log_not_found off; access_log off; } 59 | location = /robots.txt { log_not_found off; access_log off; } 60 | 61 | # Handle static files so they are not proxied to NodeJS 62 | # You may want to also hand these requests to other upstream 63 | # servers, as you can define more than one! 64 | location ~ ^/(images/|img/|javascript/|js/|css/|stylesheets/|flash/|media/|static/|robots.txt|humans.txt|favicon.ico) { 65 | root /srv/www; 66 | } 67 | 68 | # pass the request to the node.js server 69 | # with some correct headers for proxy-awareness 70 | location / { 71 | proxy_set_header X-Real-IP $remote_addr; 72 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 73 | proxy_set_header Host $http_host; 74 | proxy_set_header X-NginX-Proxy true; 75 | 76 | proxy_pass http://opendsp/; 77 | proxy_redirect off; 78 | 79 | # Handle Web Socket connections 80 | proxy_http_version 1.1; 81 | proxy_set_header Upgrade $http_upgrade; 82 | proxy_set_header Connection "upgrade"; 83 | } 84 | } 85 | --------------------------------------------------------------------------------