commit b1439a55bbc0f010cb141df4382d8a44dd800cae Author: s.golasch Date: Tue Aug 1 12:47:58 2023 +0200 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d7e2983 Binary files /dev/null and b/.DS_Store differ diff --git a/__play.js b/__play.js new file mode 100644 index 0000000..9739fea --- /dev/null +++ b/__play.js @@ -0,0 +1,32 @@ +const cookies = require('./cookie.json') +const request = require('request') + +let data = '' +let csrf = '' +cookies.forEach(cookie => { +if (cookie.name == 'csrf') csrf = cookie.value + if (cookie.name == 'csrf') console.log(cookie.value) + Object.keys(cookie).forEach(key => { + if (key == 'name') return + data += cookie.name + '=' + cookie.value + data += ';' + }) +}) +data += 'x-amzn-dat-gui-client-v=1.24.2445.0;' + +let headers = { + 'Pragma': 'no-cache', + 'Accept-Encoding': 'identity', + 'Accept-Language': 'de', + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Cache-Control': 'no-cache', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': 'https://layla.amazon.de/spa/index.html', + 'csrf': csrf, + 'Cookie': data +} +console.log(headers) + +request.post({headers, url: 'https://layla.amazon.de/api/tunein/queue-and-play?deviceSerialNumber=G090LV03644201N0&deviceType=AB72C64C86AW2&guideId=s56049&contentType=station&callSign=&mediaOwnerCustomerId=A15ZKA34OGFDCC'}, (error, response, body) => console.log(error, response.headers, body)) \ No newline at end of file diff --git a/__websocket.js b/__websocket.js new file mode 100644 index 0000000..b2325ef --- /dev/null +++ b/__websocket.js @@ -0,0 +1,103 @@ +const cookies = require('/Users/asciidisco/.alexis/cookies.json') +const request = require('request') +const WebSocket = require('faye-websocket') + +let DEVICE_SERIAL = '' +let DEVICE_TYPE = null +let data = '' +let is_connected = false + +const do_connect = (url, device_type, headers) => { + is_connected = true + let ws = new WebSocket.Client(url) + + ws.on('open', function(event) { + console.log('open'); + }); + + ws.on('message', function(event) { + console.log('message', event.data.toString()) + }); + + ws.on('close', function(event) { + console.log('close', event.code, event.reason) + ws = null; + }); +} + + +cookies.forEach(cookie => { + if (cookie.name === 'ubid-acbde') DEVICE_SERIAL = cookie.value + Object.keys(cookie).forEach(key => { + if (key == 'name') return + data += key == 'value' ? cookie.name + '=' + cookie.value : key + '=' + cookie[key] + data += ';' + }) +}) + +DEVICE_SERIAL += '-' + Date.now() + +let http_headers = { + 'Pragma': 'no-cache', + 'Accept-Encoding': 'identity', + 'Accept-Language': 'de', + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Cache-Control': 'no-cache', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': 'https://layla.amazon.de/spa/index.html', + 'Cookie': data, +} + +let ws_headers = { + 'Accept-Language': 'de', + 'Referer': 'https://layla.amazon.de/spa/index.html', + 'Cookie': data, + 'Host': 'dp-gw-na.amazon.de', + 'Origin': 'https://layla.amazon.de', + 'Pragma': 'no-cache', + 'Accept-Encoding': 'identity', + 'Upgrade': 'websocket', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36', +} + +request({headers: http_headers, url: 'https://layla.amazon.de/api/cards?limit=100'}, (err, res, body) => { + const cards = JSON.parse(body) + cards.cards.forEach(card => { + const parts = card.id.split('#') + if (parts.pop() == 'BOGUS') { + DEVICE_TYPE = parts[2] + if (!is_connected) do_connect(`wss://dp-gw-na.amazon.de/?x-amz-device-type=${DEVICE_TYPE}&x-amz-device-serial=${DEVICE_SERIAL}`, DEVICE_TYPE, ws_headers) + } + }) +}) + +/*var ws = new WebSocket.Client('wss://dp-gw-na.amazon.de/?x-amz-device-type={DEVICE_TYPE}&x-amz-device-serial={DEVICE_SERIAL}', [], { + proxy: { + origin: 'https://layla.amazon.de', + headers: {'User-Agent': 'node'} + } +})*/ + +/*client.on('connectFailed', function(error) { + console.log('Connect Error: ' + error.toString()); +}); + +client.on('connect', function(connection) { + console.log('WebSocket Client Connected'); + connection.on('error', function(error) { + console.log("Connection Error: " + error.toString()); + }); + connection.on('close', function() { + console.log('echo-protocol Connection Closed'); + }); + connection.on('message', function(message) { + if (message.type === 'utf8') { + console.log("Received: '" + message.utf8Data + "'"); + } + }); +}); + +client.connect('wss://dp-gw-na.amazon.de/?x-amz-device-type=ALEGCNGL9K0HM&x-amz-device-serial=260-2954207-0537040-1503305987401', null, ['https://layla.amazon.de', headers]); +setInterval(function () {}, 1000)*/ \ No newline at end of file diff --git a/bin/alexis b/bin/alexis new file mode 100755 index 0000000..e7bcbd2 --- /dev/null +++ b/bin/alexis @@ -0,0 +1,16 @@ +#!/usr/bin/env node +'use strict' +var cli = require('../lib/cli') +var logger = require('../lib/logger') + +// checking node version +if (parseInt(process.version.replace(/\./g, '').replace('v', ''), 10) < 600) { + console.log('Node version is not sufficient, must be Node 6 or higher') + process.exit(1) +} + +cli(function (flags, showHelp) { + var init = require('../lib/init')(logger) + var result = init.run(flags, showHelp) + if (result === false) process.exit(1) +}) \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..c29c6dd --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/init') \ No newline at end of file diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..0e4d377 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,35 @@ +'use strict' +var meow = require('meow') + +var help = '' + + 'Authenticates against the Amazon Echo Web App API.\n' + + 'Spins up a webserver (and optionally a telnet server) to access your Echo devices for remote control.\n\n' + + 'Usage:\n' + + ' $ alexis [options]\n\n' + + 'Example:\n' + + ' $ alexis --http-port=5000 --email=my@mail.com --pass=MyHopefullySuperSecretAmazonPasswort\n\n' + + 'Options:\n' + + ' --email Your Echo account e-mail\n' + + ' --pass Your Echo account password\n' + + ' --http-port [optional] Port the HTTP Server should be running on\n' + + ' --telnet-port [optional] Port the Telnet Server should be running on\n' + + ' --interval [optional] The fetch interval for querying the Echo API (default 10 sec.)\n' + + ' --no-colors [optional] Disables all colors in the terminal outoput\n' + + ' --quiet [optional] Disables all terminal output\n' + +var args = { + string: [ + 'http-port', + 'telnet-port', + 'email', + 'pass', + 'interval', + 'no-colors', + 'quiet', + ] +} + +module.exports = function (runner) { + var cli = meow(help, args) + runner(cli.flags, cli.showHelp) +} \ No newline at end of file diff --git a/lib/components/all/route.js b/lib/components/all/route.js new file mode 100644 index 0000000..2a610a7 --- /dev/null +++ b/lib/components/all/route.js @@ -0,0 +1,21 @@ +'use strict' + +const all = (store, params, cb) => { + let state = store.getState() + cb({ + todos: state.todos, + //shopping_list: state.shopping_list.items, + //timeline: state.timeline.items, + //network: state.network.network, + //devices: state.devices.devices.devices, + //household: state.household.household, + //notifications: state.notifications.notifications.notifications, + //user_data: state.user_data.userData.authentication, + //wakewords: state.wakewords.wakewords, + }) +} + +module.exports = { + '/': all, + '/all': all, +} \ No newline at end of file diff --git a/lib/components/devices/action_constants.js b/lib/components/devices/action_constants.js new file mode 100644 index 0000000..82517b6 --- /dev/null +++ b/lib/components/devices/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_DEVICES: 'RECEIVE_DEVICES', +} \ No newline at end of file diff --git a/lib/components/devices/actions.js b/lib/components/devices/actions.js new file mode 100644 index 0000000..ecdd501 --- /dev/null +++ b/lib/components/devices/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_DEVICES} = require('./action_constants') + +module.exports = { + receivedDevices: devices => { + return { + type: RECEIVE_DEVICES, + devices, + } + } +} \ No newline at end of file diff --git a/lib/components/devices/proxy.js b/lib/components/devices/proxy.js new file mode 100644 index 0000000..3d3e91f --- /dev/null +++ b/lib/components/devices/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedDevices} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/devices/device', + }, + // called after an remote http call to the device API has been successfully executed + postHooks: { + // called after the devices data has been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedDevices(data)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/devices/reducer.js b/lib/components/devices/reducer.js new file mode 100644 index 0000000..35303ff --- /dev/null +++ b/lib/components/devices/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_DEVICES} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_DEVICES: + return Object.assign({}, state, { + devices: action.devices + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/devices/route.js b/lib/components/devices/route.js new file mode 100644 index 0000000..5a4e2e5 --- /dev/null +++ b/lib/components/devices/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/devices': (store, params, cb) => { + cb(store.getState().devices.devices.devices) + } +} \ No newline at end of file diff --git a/lib/components/household/action_constants.js b/lib/components/household/action_constants.js new file mode 100644 index 0000000..a124bb4 --- /dev/null +++ b/lib/components/household/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_HOUSEHOLD: 'RECEIVE_HOUSEHOLD', +} \ No newline at end of file diff --git a/lib/components/household/actions.js b/lib/components/household/actions.js new file mode 100644 index 0000000..6eaec37 --- /dev/null +++ b/lib/components/household/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_HOUSEHOLD} = require('./action_constants') + +module.exports = { + receivedHousehold: household => { + return { + type: RECEIVE_HOUSEHOLD, + household, + } + } +} \ No newline at end of file diff --git a/lib/components/household/proxy.js b/lib/components/household/proxy.js new file mode 100644 index 0000000..998674a --- /dev/null +++ b/lib/components/household/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedHousehold} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/household', + }, + // called after an remote http call to the household API has been successfully executed + postHooks: { + // called after the household items have been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedHousehold(data)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/household/reducer.js b/lib/components/household/reducer.js new file mode 100644 index 0000000..d1ca2fe --- /dev/null +++ b/lib/components/household/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_HOUSEHOLD} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_HOUSEHOLD: + return Object.assign({}, state, { + household: action.household + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/household/route.js b/lib/components/household/route.js new file mode 100644 index 0000000..b56d598 --- /dev/null +++ b/lib/components/household/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/household': (store, params, cb) => { + res.send(store.getState().household.household) + } +} \ No newline at end of file diff --git a/lib/components/network/action_constants.js b/lib/components/network/action_constants.js new file mode 100644 index 0000000..be31c5b --- /dev/null +++ b/lib/components/network/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_NETWORK: 'RECEIVE_NETWORK', +} \ No newline at end of file diff --git a/lib/components/network/actions.js b/lib/components/network/actions.js new file mode 100644 index 0000000..9a90fb7 --- /dev/null +++ b/lib/components/network/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_NETWORK} = require('./action_constants') + +module.exports = { + receivedNetwork: network => { + return { + type: RECEIVE_NETWORK, + network, + } + } +} \ No newline at end of file diff --git a/lib/components/network/proxy.js b/lib/components/network/proxy.js new file mode 100644 index 0000000..d7006db --- /dev/null +++ b/lib/components/network/proxy.js @@ -0,0 +1,21 @@ +'use strict' + +const {receivedNetwork} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/phoenix', + discover: 'https://layla.amazon.de/api/phoenix/discovery', + }, + // called after an remote http call to the network API has been successfully executed + postHooks: { + // called after the network data has been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedNetwork(JSON.parse(data.networkDetail))) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/network/reducer.js b/lib/components/network/reducer.js new file mode 100644 index 0000000..49d4050 --- /dev/null +++ b/lib/components/network/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_NETWORK} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_NETWORK: + return Object.assign({}, state, { + network: action.network + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/network/route.js b/lib/components/network/route.js new file mode 100644 index 0000000..0f15f60 --- /dev/null +++ b/lib/components/network/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/network': (store, params, cb) => { + res.send(store.getState().network.network) + } +} \ No newline at end of file diff --git a/lib/components/notifications/action_constants.js b/lib/components/notifications/action_constants.js new file mode 100644 index 0000000..8fd4a64 --- /dev/null +++ b/lib/components/notifications/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_NOTIFICATIONS: 'RECEIVE_NOTIFICATIONS', +} \ No newline at end of file diff --git a/lib/components/notifications/actions.js b/lib/components/notifications/actions.js new file mode 100644 index 0000000..12c7479 --- /dev/null +++ b/lib/components/notifications/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_NOTIFICATIONS} = require('./action_constants') + +module.exports = { + receivedNotifications: notifications => { + return { + type: RECEIVE_NOTIFICATIONS, + notifications, + } + } +} \ No newline at end of file diff --git a/lib/components/notifications/proxy.js b/lib/components/notifications/proxy.js new file mode 100644 index 0000000..574aaf6 --- /dev/null +++ b/lib/components/notifications/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedNotifications} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/notifications', + }, + // called after an remote http call to the notifications API has been successfully executed + postHooks: { + // called after the notifications have been fetched + fetch:(store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedNotifications(data)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/notifications/reducer.js b/lib/components/notifications/reducer.js new file mode 100644 index 0000000..a2964bb --- /dev/null +++ b/lib/components/notifications/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_NOTIFICATIONS} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_NOTIFICATIONS: + return Object.assign({}, state, { + notifications: action.notifications + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/notifications/route.js b/lib/components/notifications/route.js new file mode 100644 index 0000000..880d8a3 --- /dev/null +++ b/lib/components/notifications/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/notifications': (store, params, cb) => { + cb(store.getState().notifications.notifications.notifications) + } +} \ No newline at end of file diff --git a/lib/components/shopping_list/action_constants.js b/lib/components/shopping_list/action_constants.js new file mode 100644 index 0000000..16616aa --- /dev/null +++ b/lib/components/shopping_list/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_SHOPPING_LIST: 'RECEIVE_SHOPPING_LIST', +} \ No newline at end of file diff --git a/lib/components/shopping_list/actions.js b/lib/components/shopping_list/actions.js new file mode 100644 index 0000000..24b5efd --- /dev/null +++ b/lib/components/shopping_list/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_SHOPPING_LIST} = require('./action_constants') + +module.exports = { + receivedShoppingList: todos => { + return { + type: RECEIVE_SHOPPING_LIST, + items: todos, + } + } +} \ No newline at end of file diff --git a/lib/components/shopping_list/proxy.js b/lib/components/shopping_list/proxy.js new file mode 100644 index 0000000..c5e4d86 --- /dev/null +++ b/lib/components/shopping_list/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedShoppingList} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/todos?type=SHOPPING_ITEM&size=100', + }, + // called after an remote http call to the shopping_list API has been successfully executed + postHooks: { + // called after the shopping items have been fetched + fetch:(store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedShoppingList(data.values)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/shopping_list/reducer.js b/lib/components/shopping_list/reducer.js new file mode 100644 index 0000000..8fb686b --- /dev/null +++ b/lib/components/shopping_list/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_SHOPPING_LIST} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_SHOPPING_LIST: + return Object.assign({}, state, { + items: action.items + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/shopping_list/route.js b/lib/components/shopping_list/route.js new file mode 100644 index 0000000..0f8df76 --- /dev/null +++ b/lib/components/shopping_list/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/shopping_list': (store, params, cb) => { + cb(store.getState().shopping_list.items) + } +} \ No newline at end of file diff --git a/lib/components/timeline/action_constants.js b/lib/components/timeline/action_constants.js new file mode 100644 index 0000000..9cb8694 --- /dev/null +++ b/lib/components/timeline/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_TIMELINE: 'RECEIVE_TIMELINE', +} \ No newline at end of file diff --git a/lib/components/timeline/actions.js b/lib/components/timeline/actions.js new file mode 100644 index 0000000..c7629cc --- /dev/null +++ b/lib/components/timeline/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_TIMELINE} = require('./action_constants') + +module.exports = { + receivedTimeline: timeline => { + return { + type: RECEIVE_TIMELINE, + items: timeline, + } + } +} \ No newline at end of file diff --git a/lib/components/timeline/proxy.js b/lib/components/timeline/proxy.js new file mode 100644 index 0000000..76dd816 --- /dev/null +++ b/lib/components/timeline/proxy.js @@ -0,0 +1,19 @@ +'use strict' + +const {receivedTimeline} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/cards?limit=50', + }, + postHooks: { + // called after the timeline has been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedTimeline(data.cards)) + resolve(data) + }) + }, + } +} \ No newline at end of file diff --git a/lib/components/timeline/reducer.js b/lib/components/timeline/reducer.js new file mode 100644 index 0000000..524a1c9 --- /dev/null +++ b/lib/components/timeline/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_TIMELINE} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_TIMELINE: + return Object.assign({}, state, { + items: action.items + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/timeline/route.js b/lib/components/timeline/route.js new file mode 100644 index 0000000..7f3d331 --- /dev/null +++ b/lib/components/timeline/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/timeline': (store, params, cb) => { + cb(store.getState().timeline.items) + } +} \ No newline at end of file diff --git a/lib/components/todo/action_constants.js b/lib/components/todo/action_constants.js new file mode 100644 index 0000000..f46cf73 --- /dev/null +++ b/lib/components/todo/action_constants.js @@ -0,0 +1,19 @@ +'use strict' + +const RECEIVE_TODOS = 'RECEIVE_TODOS' +const FETCH_TODOS = 'FETCH_TODOS' +const ADD_TODO = 'ADD_TODO' +const ADDED_TODO = 'ADDED_TODO' +const UPDATE_TODO = 'UPDATE_TODO' +const DELETE_TODO = 'DELETE_TODO' +const COMPLETE_TODO = 'COMPLETE_TODO' + +module.exports = { + RECEIVE_TODOS, + FETCH_TODOS, + ADD_TODO, + ADDED_TODO, + UPDATE_TODO, + DELETE_TODO, + COMPLETE_TODO, +} \ No newline at end of file diff --git a/lib/components/todo/actions.js b/lib/components/todo/actions.js new file mode 100644 index 0000000..23019ee --- /dev/null +++ b/lib/components/todo/actions.js @@ -0,0 +1,61 @@ +'use strict' + +const {FETCH_TODOS, RECEIVED_TODOS, ADD_TODO, ADDED_TODO, UPDATE_TODO, DELETE_TODO, COMPLETE_TODO} = require('./action_constants') + +const fetchTodos = todos => { + return { + type: FETCH_TODOS, + items: todos, + } +} + +const receivedTodos = todos => { + return { + type: RECEIVED_TODOS, + items: todos, + } +} + +const addTodo = todo => { + return { + type: ADD_TODO, + item: todo, + } +} + +const addedTodo = todo => { + return { + type: ADDED_TODO, + item: todo, + } +} + +const updateTodo = todo => { + return { + type: UPDATE_TODO, + item: todo, + } +} + +const deleteTodo = todo => { + return { + type: DELETE_TODO, + item: todo, + } +} + +const completeTodo = todo => { + return { + type: COMPLETE_TODO, + item: todo, + } +} + +module.exports = { + receivedTodos, + addTodo, + addedTodo, + updateTodo, + deleteTodo, + completeTodo, +} \ No newline at end of file diff --git a/lib/components/todo/proxy.js b/lib/components/todo/proxy.js new file mode 100644 index 0000000..dc02b8e --- /dev/null +++ b/lib/components/todo/proxy.js @@ -0,0 +1,61 @@ +'use strict' + +const {receivedTodos, addTodo, addedTodo} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/todos?type=TASK&size=100', + add: 'https://layla.amazon.de/api/todos', + complete: 'https://layla.amazon.de/api/todos/{ID}', + delete: 'https://layla.amazon.de/api/todos/{ID}', + update: 'https://layla.amazon.de/api/todos/{ID}', + }, + // called before an remote http call to the todos API gets executed + preHooks: { + fetch: async options => [{method: 'GET', url: options.url}], + // adds a new TODO + add: async options => { + let todoTemplate = { + text: options.text, + type: 'TASK', + itemId: null, + lastLocalUpdatedDate: null, + lastUpdatedDate: null, + createdDate: Date.now(), + utteranceId: null, + nbestItems: null, + complete: false, + version: null, + deleted: false, + reminderTime: null + } + // dispatch data about the item to be added + store.dispatch(addTodo(todoTemplate)) + // build request options + // TODO: Add error handling + return [{method: 'POST', url: options.url, body: JSON.stringify(todoTemplate)}] + } + }, + // called after an remote http call to the todos API has been successfully executed + postHooks: { + // called after the todo list has been fetched + fetch: async (store, raw_data) => { + let data = [] + try { + data = JSON.parse(raw_data) + store.dispatch(receivedTodos(data.values)) + } catch (error) { + // TODO: Add error handling + } + return data + }, + // called after the new todo has been created + add: async (store, raw_data) => { + // TODO: Add error handling + const data = JSON.parse(raw_data) + store.dispatch(addedTodo(data)) + return data + } + } +} \ No newline at end of file diff --git a/lib/components/todo/reducer.js b/lib/components/todo/reducer.js new file mode 100644 index 0000000..b7cacd1 --- /dev/null +++ b/lib/components/todo/reducer.js @@ -0,0 +1,42 @@ +'use strict' +const {RECEIVE_TODOS, RECEIVED_TODOS, ADD_TODO, ADDED_TODO} = require('./action_constants') +const INITIAL_STATE = {toBeAdded: [], items: [], size: 0, lastUpdated: null, fetching: false, error: null} + +// create an array of todos that are still queued, but not yet added +const removeToBeAdded = (current, itemToBeRemoved) => { + if (current.length === 1) return [] + return current.map(item => { + if (item.text !== itemToBeRemoved.text) return item + }) +} + +module.exports = (state = INITIAL_STATE, action) => { + switch (action.type) { + // received all todos + case RECEIVED_TODOS: + return Object.assign({}, state, { + items: action.items, + size: action.items.length, + lastUpdated: Date.now(), + }) + // trying to add a todo (request to amazon not yet made) + case ADD_TODO: + return Object.assign({}, state, { + toBeAdded: state.toBeAdded.push(action.item), + lastUpdated: Date.now(), + }) + // todo has been added successfully + case ADDED_TODO: + // remove the item from the toBeadded array + const toBeAdded = removeToBeAdded(state.toBeAdded, action.item) + state.items.push(action.item) + return Object.assign({}, state, { + items: state.items, + size: state.size + 1, + toBeAdded: toBeAdded, + lastUpdated: Date.now(), + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/todo/route.js b/lib/components/todo/route.js new file mode 100644 index 0000000..61b1e7a --- /dev/null +++ b/lib/components/todo/route.js @@ -0,0 +1,31 @@ +'use strict' + +const {receivedTodos, addTodo} = require('./actions') + +module.exports = { + // returns all todos + '/todos': (store, params, cb) => cb(store.getState().todos), + // adds a todo & returns all todos + '/todos/add': (store, params, cb, Proxy) => Proxy.act('todo', 'add', params, result => { + console.log('result', result) + cb(result) + }), + // deletes a todo & returns all todos + '/todos/delete': (store, params, cb, Proxy) => Proxy.act('todo', 'delete', params, async result => { + const data = await Proxy.fetch('todo') + store.dispatch(receivedTodos(data.todo.values)) + cb(data.todo.values) + }), + // completes a todo & returns all todos + '/todos/complete': (store, params, cb, Proxy) => Proxy.act('todo', 'complete', params, async result => { + const data = await Proxy.fetch('todo') + store.dispatch(receivedTodos(data.todo.values)) + cb(data.todo.values) + }), + // updates a todo & returns all todos + '/todos/update': (store, params, cb, Proxy) => Proxy.act('todo', 'update', params, async result => { + const data = await Proxy.fetch('todo') + store.dispatch(receivedTodos(data.todo.values)) + cb(data.todo.values) + }), +} \ No newline at end of file diff --git a/lib/components/user_data/action_constants.js b/lib/components/user_data/action_constants.js new file mode 100644 index 0000000..b7a868b --- /dev/null +++ b/lib/components/user_data/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_USER_DATA: 'RECEIVE_USER_DATA', +} \ No newline at end of file diff --git a/lib/components/user_data/actions.js b/lib/components/user_data/actions.js new file mode 100644 index 0000000..247146a --- /dev/null +++ b/lib/components/user_data/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_USER_DATA} = require('./action_constants') + +module.exports = { + receivedUserData: userData => { + return { + type: RECEIVE_USER_DATA, + userData, + } + } +} \ No newline at end of file diff --git a/lib/components/user_data/proxy.js b/lib/components/user_data/proxy.js new file mode 100644 index 0000000..0a05b13 --- /dev/null +++ b/lib/components/user_data/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedUserData} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/bootstrap?version=1.24.2445.0', + }, + // called after an remote http call to the user data API has been successfully executed + postHooks: { + // called after the user data has been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedUserData(data)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/user_data/reducer.js b/lib/components/user_data/reducer.js new file mode 100644 index 0000000..ef7b9c0 --- /dev/null +++ b/lib/components/user_data/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_USER_DATA} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_USER_DATA: + return Object.assign({}, state, { + userData: action.userData + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/user_data/route.js b/lib/components/user_data/route.js new file mode 100644 index 0000000..d911745 --- /dev/null +++ b/lib/components/user_data/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/user_data': (store, params, cb) => { + cb(store.getState().user_data.userData) + } +} \ No newline at end of file diff --git a/lib/components/wakewords/action_constants.js b/lib/components/wakewords/action_constants.js new file mode 100644 index 0000000..f489b0a --- /dev/null +++ b/lib/components/wakewords/action_constants.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + RECEIVE_WAKEWORDS: 'RECEIVE_WAKEWORDS', +} \ No newline at end of file diff --git a/lib/components/wakewords/actions.js b/lib/components/wakewords/actions.js new file mode 100644 index 0000000..b04d385 --- /dev/null +++ b/lib/components/wakewords/actions.js @@ -0,0 +1,12 @@ +'use strict' + +const {RECEIVE_WAKEWORDS} = require('./action_constants') + +module.exports = { + receivedWakewords: wakewords => { + return { + type: RECEIVE_WAKEWORDS, + wakewords, + } + } +} \ No newline at end of file diff --git a/lib/components/wakewords/proxy.js b/lib/components/wakewords/proxy.js new file mode 100644 index 0000000..88e6829 --- /dev/null +++ b/lib/components/wakewords/proxy.js @@ -0,0 +1,20 @@ +'use strict' + +const {receivedWakewords} = require('./actions') + +module.exports = { + // urls that should be called for the remote actions + urls: { + fetch: 'https://layla.amazon.de/api/wake-word', + }, + // called after an remote http call to the user data API has been successfully executed + postHooks: { + // called after the user data has been fetched + fetch: (store, data) => { + return new Promise((resolve, reject) => { + store.dispatch(receivedWakewords(data.wakeWords)) + resolve(data) + }) + } + } +} \ No newline at end of file diff --git a/lib/components/wakewords/reducer.js b/lib/components/wakewords/reducer.js new file mode 100644 index 0000000..0c157e1 --- /dev/null +++ b/lib/components/wakewords/reducer.js @@ -0,0 +1,13 @@ +'use strict' +const {RECEIVE_WAKEWORDS} = require('./action_constants') + +module.exports = (state = {}, action) => { + switch (action.type) { + case RECEIVE_WAKEWORDS: + return Object.assign({}, state, { + wakewords: action.wakewords + }) + default: + return state + } +} \ No newline at end of file diff --git a/lib/components/wakewords/route.js b/lib/components/wakewords/route.js new file mode 100644 index 0000000..c6a55ac --- /dev/null +++ b/lib/components/wakewords/route.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + '/wakewords': (store, params, cb) => { + cb(store.getState().wakewords.wakewords) + } +} \ No newline at end of file diff --git a/lib/cookies.js b/lib/cookies.js new file mode 100644 index 0000000..c9f1755 --- /dev/null +++ b/lib/cookies.js @@ -0,0 +1,69 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const request =require('request') +const Headers = require('./headers') + +// files & endpoints +const RAW_COOKIE_FILE = 'raw_jar.txt' +const JSON_COOKIE_FILE = 'cookies.json' +const TEST_ENDPOINT = 'https://layla.amazon.de/api/devices/device' + +// Returns the full raw cookies path +const getRawCookiePath = data_dir => path.join(data_dir, RAW_COOKIE_FILE) +// Checks if the JSON cookie file exists +const getJsonCookiePath = data_dir => path.join(data_dir, JSON_COOKIE_FILE) +// Checks if the raw cookie file exists +const checkRawCookiesExist = data_dir => fs.existsSync(getRawCookiePath(data_dir)) +// Checks if the JSON cookie file exists +const checkJsonCookiesExist = data_dir => fs.existsSync(getJsonCookiePath(data_dir)) +// Checks if the raw cookie file exists +const deleteRawCookies = data_dir => fs.removeSync(getRawCookiePath(data_dir)) +// Checks if the JSON cookie file exists +const deleteJsonCookies = data_dir => fs.removeSync(getJsonCookiePath(data_dir)) +// Returns the cookies as a header compatible string +const transformCookiesForHeader = json_cookies => { + let string_cookies = '' + json_cookies.forEach(cookie => { + Object.keys(cookie).forEach(key => { + if (key == 'name') return + string_cookies += key == 'value' ? cookie.name + '=' + cookie.value : key + '=' + cookie[key] + string_cookies += ';' + }) + }) + return string_cookies +} +// Validate the cookie +const validateCookie = (data_dir) => { + const json_cookies = require(path.join(data_dir, JSON_COOKIE_FILE)) + const string_cookies = transformCookiesForHeader(json_cookies) + const headers = Headers.getFetchHeaders(string_cookies) + return new Promise((resolve, reject) => { + request.get({headers, url: TEST_ENDPOINT}, (error, response, body) => { + try { + let contents = JSON.parse(body) + if (contents.devices) { + resolve([true, contents]) + } else { + reject([false, null]) + } + } catch (e) { + reject([false, body]) + } + }) + }) +} + +module.exports = { + getRawCookiePath, + getJsonCookiePath, + checkRawCookiesExist, + checkJsonCookiesExist, + deleteRawCookies, + deleteJsonCookies, + validateCookie, + transformCookiesForHeader, + RAW_COOKIE_FILE, + JSON_COOKIE_FILE, +} \ No newline at end of file diff --git a/lib/headers.js b/lib/headers.js new file mode 100644 index 0000000..74382ee --- /dev/null +++ b/lib/headers.js @@ -0,0 +1,28 @@ +'use strict' + +const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4' + +const base_headers = { + 'Pragma': 'no-cache', + 'Accept-Encoding': 'identity', + 'Accept-Language': 'de', + 'User-Agent': ua, + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Cache-Control': 'no-cache', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': 'https://layla.amazon.de/spa/index.html', +} + +const getActionHeaders = (json_cookies, cookies) => { + var csrf = '252916842' + return Object.assign({}, getFetchHeaders(cookies + 'csrf=' + csrf + ';'), {csrf}) +} + +const getFetchHeaders = (cookies) => Object.assign({}, base_headers, {'Cookie': cookies}) + +module.exports = { + ua, + getActionHeaders, + getFetchHeaders, +} \ No newline at end of file diff --git a/lib/init.js b/lib/init.js new file mode 100644 index 0000000..16f5287 --- /dev/null +++ b/lib/init.js @@ -0,0 +1,218 @@ +'use strict' + +const os = require('os') +const fs = require('fs') +const path = require('path') +const homedir = require('homedir') +const Store = require('./store') +const phantomloader = require('./phantomloader') +const webserver = require('./webserver') +const Cookies = require('./cookies') +const Headers = require('./headers') +const Login = require('./login') +const proxy = require('./proxy') + +// file & directory names +const DATA_DIR = '.alexis' +const PHANTOMFILE = os.platform() === 'win32' ? 'phantomjs.exe' : 'phantomjs' + +// computes the data directory +const getDataDir = () => path.join(homedir(), DATA_DIR) +// checks if the data directory exists +const checkDataDirExists = () => fs.existsSync(getDataDir()) +// creates the data directory +const createDataDir = () => fs.mkdirSync(getDataDir()) +// checks if the PhantomJS binary exists +const checkPhantomExist = () => fs.existsSync(path.join(getDataDir(), PHANTOMFILE)) +// checks if the given options are valid +const validateOptions = (options, verbose) => { + let missing = []; + ['httpPort', 'email', 'pass'].forEach(option => { + if (!options[option]) missing.push(option) + }) + if (verbose && missing.length > 0) return missing.join(', ') + if (missing.length > 0) return false + return true +} +// validate options +const optionsAreValid = (options = {}, invalidOptionsCb) => { + if (!options.interval) options.interval = 10 + options.interval = options.interval * 1000 + if(!validateOptions(options)) { + invalidOptionsCb() + return false + } + return true +} +// checks if data dir exists and creates it if not +const doCreateUserdataDirectory = (data_dir, logger) => { + if (!checkDataDirExists()) { + logger.log('SETUP', `Creating data directory: ${data_dir}`) + createDataDir() + } +} +// checks if Phantom is installed, downloads it if not +const mountPhantom = async (data_dir, logger) => { + if (!checkPhantomExist()) { + phantom_mounted = false + const phantom_path_name = path.join(data_dir, PHANTOMFILE) + logger.log('SETUP', 'Downloading Phantom') + return await Phantom.download(phantom_path_name, data_dir) + } + return true +} + +// Initializing flow +const run = async (logger, options, invalidOptionsCb) => { + // helper variable to determine if cookies exist + let cookies_exist = false + // helper variable to determine if cookies are valid + let cookies_valid = false + // helper variable to determine if login did work + let login_success = false + // init store + let store = null + // device data store + let device_data = null + // init PhantomJS downlaoder + const Phantom = phantomloader(logger) + // cache data dir location + const data_dir = getDataDir() + + // validate options + if (!optionsAreValid(options, invalidOptionsCb)) return false + + // set up redux store + logger.log('SETUP', 'Creating in-memory store') + store = Store() + + // check if data dir exists, else create + doCreateUserdataDirectory(data_dir, logger) + + // checks if Phantom is installed, downloads it if not + const phantom_mounted = await mountPhantom(data_dir, logger) + + // check if phantom is mounted + if (phantom_mounted !== true) { + logger.log('ERROR', 'Error with mounting Phantom') + return false + } + + // everythings okay + logger.log('SETUP', 'Dependency check successful') + + // cookie check + if (Cookies.checkJsonCookiesExist(data_dir) && Cookies.checkJsonCookiesExist(data_dir)) { + logger.log('SETUP', 'Cookies exist, trying to reuse them') + // validate existing cookies + try { + cookies_valid = await Cookies.validateCookie(data_dir) + device_data = cookies_valid[1] + cookies_valid = cookies_valid[0] + } catch (e) { + cookies_valid = false + } + + // set flasg & log cookie state + if (cookies_valid) { + cookies_exist = true + login_success = true + logger.log('SETUP', 'Cookies valid, connection could be established') + } else { + cookies_exist = false + logger.log('SETUP', 'Cookies invalid, connection could not be established') + } + } + + // create a new set of cookies by logging in + if (!cookies_exist) { + logger.log('SETUP', 'No cookies found or invalid, logging in to create a fresh pair') + try { + login_success = await Login.fetchInitialCookies( + Cookies.getJsonCookiePath(data_dir), + Cookies.getRawCookiePath(data_dir), + Headers.ua, + options.email, + options.pass, + path.join(data_dir, PHANTOMFILE), + ) + } catch (error) { + console.log(error) + if (error === 'captcha') { + logger.log('ERROR', 'Login failed, we got a captcha notice. Please change your public IP and try again.') + } else { + logger.log('ERROR', 'Login failed, please check your credentials') + } + login_success = false + return false + } + // Login should´ve been successful, lets validate + logger.log('SETUP', 'Login successful') + + // cookie check + if (Cookies.checkJsonCookiesExist(getDataDir()) && Cookies.checkJsonCookiesExist(getDataDir())) { + // validate existing cookies + try { + cookies_valid = await Cookies.validateCookie(getDataDir()) + device_data = cookies_valid[1] + cookies_valid = cookies_valid[0] + } catch (e) { + cookies_valid = false + } + + // set flasg & log cookie state + if (cookies_valid) { + logger.log('SETUP', 'Cookies valid, connection could be established') + } else { + logger.log('SETUP', 'Cookies invalid, connection could not be established') + return false + } + } + } + + // set up proxy dialer + const Proxy = await proxy(logger, data_dir, store) + + // initial fetch of data + logger.log('SETUP', 'Fetching initial data') + const initial_data = await Proxy.fetchAll() + logger.log('SETUP', 'Fetch finished') + + // start webserver + logger.log('SETUP', 'Starting webserver') + try { + await webserver(logger, options.httpPort, store, Proxy) + } catch (error) { + logger.log('ERROR', `Problem starting webserver: ${error}`) + return false + } + + // set up scheduler to periodically fetch data + logger.log('SETUP', `Setting up scheduler with interval: ${options.interval}ms`) + + // display data about the echo devices we found + logger.log('ECHO', `Found ${device_data.devices.length} Device(s) connected with this account`) + device_data.devices.forEach(device => { + logger.log('ECHO', `---------------------------------`) + logger.log('ECHO', `Name: ${device.accountName}`) + logger.log('ECHO', `Id: ${device.deviceAccountId}`) + logger.log('ECHO', `Type: ${device.deviceFamily}`) + logger.log('ECHO', `Active: ${device.online}`) + logger.log('ECHO', `Mac Address: ${device.macAddress}`) + }) + logger.log('ECHO', `---------------------------------`) + + // setup done, system is ready to use + logger.log('SETUP', 'Setup finished. Ready to use. Have Fun!') + return true +} + +module.exports = (logger) => { + return { + getDataDir, + checkDataDirExists, + createDataDir, + checkPhantomExist, + run: run.bind(this, logger) + } +} \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..9f7345e --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,48 @@ +'use strict' + +var chalk = require('chalk') + +// colorize keywords +var keywordMap = { + SETUP: chalk.black.bgGreen.bold, + PHANTOMJS: chalk.black.bgMagenta.bold, + ROUTES: chalk.white.bgBlue.bold, + WEBSERVER: chalk.black.bgCyan.bold, + ERROR: chalk.black.bgRed.bold, + ECHO: chalk.black.bgYellowBright.bold, +} + +// output formatted datetime before each log entry +var formatConsoleDate = () => { + var date = new Date() + var hour = date.getHours() + var minutes = date.getMinutes() + var seconds = date.getSeconds() + var milliseconds = date.getMilliseconds() + return ((hour < 10) ? '0' + hour: hour) + ':' + + ((minutes < 10) ? '0' + minutes: minutes) + ':' + + ((seconds < 10) ? '0' + seconds: seconds) + '.' + + ('00' + milliseconds).slice(-3) +} + +// log with colour +var logWithColour = function(item, text) { + console.log( + chalk.black.bgWhite(formatConsoleDate()), + keywordMap[item]('[' + item + ']'), + text) +} + +// log without colour +var logWithoutColour = function(item, text) { + console.log(formatConsoleDate(), '[' + item + ']', text) +} + +// export the log function +module.exports = { + setOption: function (use_colour, quiet) { + if (use_colour === false) this.log = logWithoutColour + if (quiet === true) this.log = function () {} + }, + log: logWithColour +} \ No newline at end of file diff --git a/lib/login.js b/lib/login.js new file mode 100644 index 0000000..0b71ce8 --- /dev/null +++ b/lib/login.js @@ -0,0 +1,32 @@ +'use strict' + +const fs = require('fs') +const Horseman = require('node-horseman') + +const LOGIN_PAGE_URL = 'https://www.amazon.de/ap/signin?showRmrMe=1&openid.return_to=https%3A%2F%2Flayla.amazon.de&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=amzn_dp_project_dee_de&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&' + +const fetchInitialCookies = (jsonCookiesFile, cookiesFile, useragent, user, password, phantomPath) => { + const horseman = new Horseman({cookiesFile, phantomPath}) + return new Promise((resolve, reject) => { + horseman + .userAgent(useragent) + .open(LOGIN_PAGE_URL) + .text('#auth-fpp-link-bottom') + .type('#ap_email', user) + .type('#ap_password', password) + .click('[name="rememberMe"]') + .click('#signInSubmit') + .waitForNextPage() + .plainText() + .then((text) => { + if (text.search('Anmelden') !== -1) return reject(false) + }) + .cookies() + .then(cookies => resolve(fs.writeFileSync(jsonCookiesFile, JSON.stringify(cookies)))) + .close() + }) +} + +module.exports = { + fetchInitialCookies, +} \ No newline at end of file diff --git a/lib/phantomloader.js b/lib/phantomloader.js new file mode 100644 index 0000000..97a75e2 --- /dev/null +++ b/lib/phantomloader.js @@ -0,0 +1,177 @@ +'use strict' + +const os = require('os') +const url = require('url') +const path = require('path') +const cp = require('child_process') +const fs = require('fs-extra') +const request = require('request') +const extractZip = require('extract-zip') + +const PHANTOM_VERSION = '2.1.1' +const DEFAULT_CDN = 'https://github.com/Medium/phantomjs/releases/download/v2.1.1' +const ARM_URL = 'https://github.com/fg2it/phantomjs-on-raspberry/raw/master/rpi-1-2-3/wheezy-jessie/v2.1.1/phantomjs' +const PLATFORM = os.platform() +const ARCH = os.arch() + +const getDownloadUrl = () => { + let downloadUrl = DEFAULT_CDN + '/phantomjs-' + PHANTOM_VERSION + '-' + if (PLATFORM === 'linux' && ARCH === 'x64') { + downloadUrl += 'linux-x86_64.tar.bz2' + } else if (PLATFORM === 'linux' && ARCH == 'ia32') { + downloadUrl += 'linux-i686.tar.bz2' + } else if (PLATFORM === 'linux' && ARCH.search('arm') !== -1) { + downloadUrl = ARM_URL + } else if (PLATFORM === 'darwin') { + downloadUrl += 'macosx.zip' + } else if (PLATFORM === 'win32') { + downloadUrl += 'windows.zip' + } else { + return null + } + return downloadUrl +} + +const downloadPhantomjs = (logger, downloadUrl, data_dir) => { + return new Promise((resolve, reject) => { + const file_name = downloadUrl.split('/').pop() + const downloaded_file = path.join(data_dir, file_name) + // actually downloads the file + const startDownload = (url, dest, cb) => { + const file = fs.createWriteStream(dest) + // download file + request(url) + .pipe(file) + .on('error', cb) + + file.on('close', () => { + // close file & invoke callback + file.close() + cb() + }) + } + startDownload(downloadUrl, downloaded_file, err => { + if (err) return reject(err) + resolve(downloaded_file) + }) + }) +} + +const extractDownload = (logger, filePath) => { + return new Promise((resolve, reject) => { + var extractedPath = filePath + '-extract-' + Date.now() + var options = {cwd: extractedPath} + fs.mkdirsSync(extractedPath, '0777') + // Make double sure we have 0777 permissions; some operating systems + // default umask does not allow write by default. + fs.chmodSync(extractedPath, '0777') + if (filePath.substr(-4) === '.zip') { + logger.log('PHANTOMJS','Extracting zip contents') + extractZip(path.resolve(filePath), {dir: extractedPath}, err => { + if (err) { + reject(err) + } else { + resolve(extractedPath) + } + }) + } else { + logger.log('PHANTOMJS', 'Extracting tar contents (via spawned process)') + cp.execFile('tar', ['jxf', path.resolve(filePath)], options, err => { + if (err) { + reject(err) + } else { + resolve(extractedPath) + } + }) + } + }) +} + +const copyIntoPlace = (logger, extractedPath, archiveFile, targetPath) => { + return new Promise((resolve, reject) => { + logger.log('PHANTOMJS', `Removing ${archiveFile}`) + fs.removeSync(archiveFile) + const folders = fs.readdirSync(extractedPath) + const bin_dir = path.join(extractedPath, folders[0], 'bin') + const files = fs.readdirSync(path.join(extractedPath, folders[0], 'bin')) + let moved = false + for (let i = 0; i < files.length; i++) { + let file = path.join(bin_dir, files[i]) + let filename = files[i] + let target = targetPath + path.sep + filename + logger.log('PHANTOMJS', `Copying extracted binary ${file} -> ${target}`) + fs.moveSync(file, target) + logger.log('PHANTOMJS', `Removing temporary download folder ${extractedPath}`) + fs.removeSync(extractedPath) + moved = target + } + + if (moved !== false) { + resolve(moved) + } else { + reject() + } + }) +} + +const download = async (logger, phantom_bin, data_dir) => { + const download_url = getDownloadUrl() + let downloaded_file = '' + let extracted_folder = '' + let moved_file = '' + // error out if we couldn't find a suitable platform/arch combination + if (download_url === null) { + logger.log('ERROR', + `Unexpected platform or architecture: ${PLATFORM} / ${ARCH}\n` + + 'It seems there is no binary available for your platform/architecture\n' + + `Try to install PhantomJS on your own & copy/link the executable to "${data_dir}"`) + return false + } + logger.log('PHANTOMJS', `Downloading from "${download_url}" to "${data_dir}/"`) + + // download phantomjs + try { + downloaded_file = await downloadPhantomjs(logger, download_url, data_dir) + logger.log('PHANTOMJS', 'Download finished') + } catch (e) { + logger.log('ERROR', 'Error downloading PhantomJS - ' + e) + return false + } + + // extract phantomjs (if needed) + // if it´s ARM, we´re done now + if (ARCH.search('arm') !== -1) return true + logger.log('PHANTOMJS', `Extracting PhantomJS binary: ${downloaded_file}`) + try { + extracted_folder = await extractDownload(logger, downloaded_file) + } catch (e) { + logger.log('ERROR', 'Error extracting PhantomJS - ' + e) + return false + } + + logger.log('PHANTOMJS', 'Successfully extracted archive: "${extracted_folder}"') + + // move Phantom executable + logger.log('PHANTOMJS', 'Moving executable to top level') + try { + moved_file = await copyIntoPlace(logger, extracted_folder, downloaded_file, data_dir) + } catch (e) { + logger.log('PHANTOMJS', 'Could not find extracted file') + return false + } + + // make phantomjs executable + try { + fs.chmodSync(moved_file, '755') + } catch (e) { + logger.log('PHANTOMJS', 'Possible error with rights of downloaded Phantom binary') + } + + return true +} + +module.exports = (logger) => { + return { + download: download.bind(this, logger) + } +} \ No newline at end of file diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 0000000..44b181f --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,93 @@ +'use strict' + +const path = require('path') +const klaw = require('klaw') +const request = require('request') +const through2 = require('through2') +const Cookies = require('./cookies') +const Headers = require('./headers') + +// check if a resource is available +const checkForResource = (fetchers, resource) => !fetchers[resource] +// check if a ressources subressource has a registered preHook +const checkForPostHooks = (fetchers, resource, subressource) => fetchers[resource].postHooks && fetchers[resource].postHooks[subressource] +// check if a ressources subressource has a registered preHook +const checkForPreHooks = (fetchers, resource, subressource) => fetchers[resource].preHooks && fetchers[resource].preHooks[subressource] +// resource not found +const resourceNotFound = (reject, resource) => reject(`Resource not found: ${resource}`) +// helper stream transform to exclude all non proxy files from components +const excludeNonComponentProxies = through2.obj(function (item, enc, next){ + if (path.basename(item.path) === path.basename(__filename) && item.path !== __filename) this.push(item) + next() +}) +// register all component proxies +const registerProxies = () => { + return new Promise((resolve, reject) => { + let items = {} + klaw(__dirname) + .pipe(excludeNonComponentProxies) + .on('data', item => items[path.dirname(item.path).split(path.sep).pop()] = require(item.path)) + .on('error', reject) + .on('end', resolve.bind(null, items)) + }) +} +// get array of requests that should be made +const getActions = async (fetchers, resource, subressource, params) => { + if (checkForPreHooks(fetchers, resource, subressource)) { + return await fetchers[resource].preHooks[subressource](Object.assign({}, params, {url: fetchers[resource].urls[subressource]})) + } else { + return [] + } +} +// called when the resquest is done +const requestDone = async (resolve, reject, fetchers, store, resource, subressource, err, response, body) => { + if (err) return reject(err) + try { + if (checkForPostHooks(fetchers, resource, subressource)) body = await fetchers[resource].postHooks[subressource](store, body) + resolve({[resource]: body}) + } catch (error) { + reject(error) + } +} +// fetch one ressource +const fetch = async (logger, headers, fetchers, store, resource, subressource = 'fetch', params = {}) => { + if (checkForResource(fetchers, resource)) return resourceNotFound(reject, resource) + const actions = await getActions(fetchers, resource, subressource) + const requests = actions.map(action => new Promise((resolve, reject) => request(Object.assign({}, {headers}, action), requestDone.bind(null, resolve, reject, fetchers, store, resource, subressource)))) + const data = await Promise.all(requests) + return data +} + +// fetch all registered resources +const fetchAll = (logger, headers, fetchers, store) => Promise.all(Object.keys(fetchers).map(resource => fetch(logger, headers, fetchers, store, resource))) + +// fetch resources periodically +const fetchInterval = (logger, headers, fetchers, store, interval = 10000) => setInterval(() => resolveInterval(fetchAll(logger, headers, fetchers)), interval) + +// fire actions +const act = async (logger, headers, fetchers, store, resource, subresource, params, cb) => { + if (checkForResource(fetchers, resource) || !checkForPreHooks(fetchers, resource, subresource)) return resourceNotFound(reject, resource) + const actions = await fetchers[resource].preHooks[subresource](Object.assign({}, params, {url: fetchers[resource].urls[subresource]})) + const response = () => actions.map(action => { + return new Promise((resolve, reject) => request(Object.assign({}, {headers}, action), requestDone.bind(null, resolve, reject, fetchers, store, resource, subresource))) + }) + const res = await Promise.all(response()) + console.log('res', res) + cb(res) +} + +module.exports = async (logger, data_dir, store, interval) => { + const json_cookies = require(path.join(data_dir, Cookies.JSON_COOKIE_FILE)) + const cookies = Cookies.transformCookiesForHeader(json_cookies) + const headers = Headers.getFetchHeaders(cookies) + const action_headers = Headers.getActionHeaders(json_cookies, cookies) + const fetchers = await registerProxies() + return new Promise((resolve, reject) => { + resolve({ + act: act.bind(this, logger, action_headers, fetchers, store), + fetch: fetch.bind(this, logger, headers, fetchers, store), + fetchAll: fetchAll.bind(this, logger, headers, fetchers, store), + fetchInterval: fetchInterval.bind(this, logger, headers, fetchers, store, interval), + }) + }) +} \ No newline at end of file diff --git a/lib/reducer.js b/lib/reducer.js new file mode 100644 index 0000000..4f2aea4 --- /dev/null +++ b/lib/reducer.js @@ -0,0 +1,28 @@ +'use strict' + +const {combineReducers} = require('redux') + +// load sub reducers from components +const todos = require('./components/todo/reducer') +const shopping_list = require('./components/shopping_list/reducer') +const timeline = require('./components/timeline/reducer') +const network = require('./components/network/reducer') +const devices = require('./components/devices/reducer') +const household = require('./components/household/reducer') +const notifications = require('./components/notifications/reducer') +const user_data = require('./components/user_data/reducer') +const wakewords = require('./components/wakewords/reducer') + +module.exports = () => { + return combineReducers({ + todos, + shopping_list, + timeline, + network, + devices, + household, + notifications, + user_data, + wakewords, + }) +} \ No newline at end of file diff --git a/lib/store.js b/lib/store.js new file mode 100644 index 0000000..40fdcb2 --- /dev/null +++ b/lib/store.js @@ -0,0 +1,12 @@ +'use strict' + +const {createStore, applyMiddleware} = require('redux') +const rootReducer = require('./reducer') + +// local shadowed store +let store = null + +module.exports = () => { + if (store === null) store = createStore(rootReducer(), {}) + return store +} diff --git a/lib/telnetserver.js b/lib/telnetserver.js new file mode 100644 index 0000000..c12c46c --- /dev/null +++ b/lib/telnetserver.js @@ -0,0 +1,43 @@ +'use strict' + +const net = require('net') + +let server = null +let sockets = [] + +// cleans the input of carriage return, newline +const cleanInput = data => data.toString().replace(/(\r\n|\n|\r)/gm, '') + +// executed when data is received from a socket +const receiveData = (socket, data) => { + const cleanData = cleanInput(data) + if (cleanData === "@quit") { + socket.end('Goodbye!\n') + } else { + for (let i = 0; i < sockets.length; i++) { + if (sockets[i] !== socket) { + sockets[i].write(data) + } + } + } +} + +// executed when a socket ends +const closeSocket = socket => { + const i = sockets.indexOf(socket) + if (i != -1) sockets.splice(i, 1) +} + +// callback method executed when a new TCP socket is opened +const newSocket = socket => { + sockets.push(socket) + socket.write('Welcome to the Telnet server!\n') + socket.on('data', data => receiveData(socket, data)) + socket.on('end', () => closeSocket(socket)) +} + +// create a new server +module.exports = (port) => { + if (server === null) server = net.createServer(newSocket).listen(port) + return server +} \ No newline at end of file diff --git a/lib/webserver.js b/lib/webserver.js new file mode 100644 index 0000000..2918af8 --- /dev/null +++ b/lib/webserver.js @@ -0,0 +1,55 @@ +'use strict' + +const express = require('express') +const app = express() + +// routes +const all = require('./components/all/route') +const todos = require('./components/todo/route') +const shopping_list = require('./components/shopping_list/route') +const timeline = require('./components/timeline/route') +const network = require('./components/network/route') +const devices = require('./components/devices/route') +const household = require('./components/household/route') +const notifications = require('./components/notifications/route') +const user_data = require('./components/user_data/route') +const wakewords = require('./components/wakewords/route') + +module.exports = (logger, port, store, Proxy) => { + return new Promise((resolve, reject) => { + // collect routes + const components = [ + all, + todos, + shopping_list, + timeline, + network, + devices, + household, + notifications, + user_data, + wakewords, + ] + + // register routes + components.forEach(component => { + Object.keys(component).forEach(route => { + logger.log('ROUTES', `(webserver) Register route ${route}`) + app.get(route, (req, res) => { + component[route](store, req.query, (data) => { + res.send(data) + }, Proxy) + }) + }) + }) + + // listen on errors + app.on('error', reject) + + // start webserver + app.listen(port, () => { + logger.log('WEBSERVER', `Webserver started: http://localhost:${port}/`) + resolve(true) + }) + }) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ac3f9ec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1378 @@ +{ + "name": "alexis", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "requires": { + "mime-types": "2.1.16", + "negotiator": "0.6.1" + } + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "requires": { + "color-convert": "1.9.0" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", + "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.2.1" + } + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=" + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", + "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookies.txt": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookies.txt/-/cookies.txt-0.1.2.tgz", + "integrity": "sha1-qLJJ2e6WmTBTYtYEt+CsDyDnUZ4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "1.0.2" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "data-uri-to-buffer": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.4.tgz", + "integrity": "sha1-RuE6udqOMJdFyNAc5UchPr2y/j8=" + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "1.0.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "express": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.4.tgz", + "integrity": "sha1-Ay4iU0ic+PzgJma+yj0R7XotrtE=", + "requires": { + "accepts": "1.3.3", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "1.0.2", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.8", + "depd": "1.1.1", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.0", + "finalhandler": "1.0.4", + "fresh": "0.5.0", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.1", + "path-to-regexp": "0.1.7", + "proxy-addr": "1.1.5", + "qs": "6.5.0", + "range-parser": "1.2.0", + "send": "0.15.4", + "serve-static": "1.12.4", + "setprototypeof": "1.0.3", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.0", + "vary": "1.1.1" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.0.tgz", + "integrity": "sha512-fjVFjW9yhqMhVGwRExCXLhJKrLlkYSaxNWdyc9rmHlrVZbk35YHH312dFd7191uQeXkI3mKLZTIbSvIeFwFemg==" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extract-zip": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.5.tgz", + "integrity": "sha1-maBnNbbqIOqbcF13ms/8yHz/BEA=", + "requires": { + "concat-stream": "1.6.0", + "debug": "2.2.0", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "requires": { + "websocket-driver": "0.6.5" + } + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "requires": { + "pend": "1.2.0" + } + }, + "finalhandler": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.4.tgz", + "integrity": "sha512-16l/r8RgzlXKmFOhZpHBztvye+lAhC5SU7hXavnerC9UfZqZxxXl3BzL8MhffPT3kF61lj9Oav2LKEzh0ei7tg==", + "requires": { + "debug": "2.6.8", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.1", + "statuses": "1.3.1", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.16" + } + }, + "forwarded": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" + }, + "fresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "fs-extra": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.1.tgz", + "integrity": "sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.1" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "homedir": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/homedir/-/homedir-0.6.0.tgz", + "integrity": "sha1-KyHbZr8Ipts4JJo+/1LX0YcGrx4=" + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "2.0.1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.4.0.tgz", + "integrity": "sha1-KWrKh4qCGBbluF0KKFqZvP9FgvA=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "klaw": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.1.0.tgz", + "integrity": "sha1-aUomkBn0Mh2SM/sbmr2uIeOCWfs=", + "requires": { + "graceful-fs": "4.1.11" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash-es": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz", + "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc=" + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", + "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=" + }, + "mime-types": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", + "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", + "requires": { + "mime-db": "1.29.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "node-horseman": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-horseman/-/node-horseman-3.3.0.tgz", + "integrity": "sha1-hhADaUN955T2aeD1L9lM+UWYG7I=", + "requires": { + "bluebird": "3.5.0", + "clone": "1.0.2", + "cookies.txt": "0.1.2", + "data-uri-to-buffer": "0.0.4", + "debug": "2.2.0", + "defaults": "1.0.3", + "node-phantom-simple": "2.2.4" + } + }, + "node-phantom-simple": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/node-phantom-simple/-/node-phantom-simple-2.2.4.tgz", + "integrity": "sha1-T8Tv+7AvJB+1CCvU+6s5jkrstk0=", + "requires": { + "debug": "2.2.0" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "1.3.1" + } + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "proxy-addr": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.5.tgz", + "integrity": "sha1-ccDuOxAt4/IC87ZPYI0XP8uhqRg=", + "requires": { + "forwarded": "0.1.0", + "ipaddr.js": "1.4.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "4.17.4", + "lodash-es": "4.17.4", + "loose-envify": "1.3.1", + "symbol-observable": "1.0.4" + } + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.16", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "send": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.4.tgz", + "integrity": "sha1-mF+qPihLAnPHkzZKNcZze9k5Bbk=", + "requires": { + "debug": "2.6.8", + "depd": "1.1.1", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.0", + "fresh": "0.5.0", + "http-errors": "1.6.2", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "serve-static": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.4.tgz", + "integrity": "sha1-m2qpjutyU8Tu3Ewfb9vKYJkBqWE=", + "requires": { + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.1", + "send": "0.15.4" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", + "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", + "requires": { + "has-flag": "2.0.0" + } + }, + "symbol-observable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "requires": { + "punycode": "1.4.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.16" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedarray-to-buffer": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz", + "integrity": "sha1-EBezLZhP9VbroQD1AViauhrOLgQ=", + "requires": { + "is-typedarray": "1.0.0" + } + }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "websocket": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.24.tgz", + "integrity": "sha1-dJA+dfJUW2suHeFCW8HJBZF6GJA=", + "requires": { + "debug": "2.2.0", + "nan": "2.6.2", + "typedarray-to-buffer": "3.1.2", + "yaeti": "0.0.6" + } + }, + "websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "requires": { + "websocket-extensions": "0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", + "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "requires": { + "fd-slicer": "1.0.1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ea3d38 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "alexis", + "version": "1.0.0", + "description": "Local proxy server to remote control your Amazon Echo devices", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Sebastian Golasch (http://asciidisco.com/)", + "license": "MIT", + "dependencies": { + "chalk": "2.1.0", + "express": "4.15.4", + "extract-zip": "1.6.5", + "faye-websocket": "0.11.1", + "fs-extra": "4.0.1", + "homedir": "0.6.0", + "klaw": "2.1.0", + "meow": "3.7.0", + "node-horseman": "3.3.0", + "redux": "3.7.2", + "request": "2.81.0", + "through2": "2.0.3", + "websocket": "1.0.24" + } +}