Skip to content
This repository has been archived by the owner on Oct 14, 2020. It is now read-only.

Commit

Permalink
Add /extensions endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
bbondy committed Sep 26, 2016
1 parent 1ce6f0b commit 8838740
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"mongodb": "^2.1.4",
"newrelic": "^1.25.3",
"underscore": "^1.8.3",
"xmldoc": "^0.5.1",
"yargs": "^3.31.0"
},
"devDependencies": {
Expand Down
96 changes: 96 additions & 0 deletions src/controllers/extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const builder = require('xmlbuilder')
const xmldoc = require('xmldoc')
const {comparableVersion} = require('../common')

/**
* Extracts an array of requested extensions along with their version
*
* @param @requestXML - The input extension request XML protocol 3.0
* @return undefined if there was an error parsing the document, or an array of
* [extensionId, extensionVersion] if successful.
*/
const getRequestedExtensions = (requestXML) => {
const doc = new xmldoc.XmlDocument(requestXML)
if (doc.attr.protocol !== '3.0') {
console.error('Only protocol v3 is supproted')
return undefined
}
const extensions = doc.childrenNamed('app')
.map((app) => [app.attr.appid, app.attr.version])
return extensions
}

/**
* Filters out to only the availableExtensions that should be updated for the request.
* For example some extensions may not be requested, and some may already have a fully
* updated, or even newer versions.
*/
const getExtensionsWithUpdates = (availableExtensions, requestedExtensions) =>
requestedExtensions.reduce((resultExtensions, requestedExtension) => {
const foundExtension = availableExtensions.find((extension) => extension[0] === requestedExtension[0])
if (foundExtension) {
if (comparableVersion(foundExtension[1]) > comparableVersion(requestedExtension[1])) {
resultExtensions.push(foundExtension)
}
}
return resultExtensions
}, [])

const getExtensionsResponse = (baseCRXUrl, extensions) => {
const doc = builder
.create('response')
.att('protocol', '3.0')
.att('server', 'prod')
extensions.forEach(([extensionId, extensionVersion, extensionSHA256]) => {
doc.ele('app')
.att('appid', extensionId)
.ele('updatecheck')
.att('status', 'ok')
.ele('urls')
.ele('url')
.att('codebase', `${baseCRXUrl}/${extensionId}/extension_${extensionVersion.replace(/\./g, '_')}.crx`)
.up()
.up()
.ele('manifest')
.att('version', extensionVersion)
.ele('packages')
.ele('package')
.att('name', `extension_${extensionVersion.replace(/\./g, '_')}.crx`)
.att('hash_sha256', extensionSHA256)
.att('required', true)
.up()
.up()
.up()
})
return doc.toString({ pretty: true })
}

const setup = (runtime, availableExtensions) => {
let extensionsRoute = {
method: ['POST'],
path: '/extensions',
config: {
handler: function (request, reply) {
const requestedExtensions = getRequestedExtensions(request.payload.toString())
const extensionsWithUpdates = getExtensionsWithUpdates(availableExtensions, requestedExtensions)
reply(getExtensionsResponse('https://s3.amazonaws.com/brave-extensions/release', extensionsWithUpdates))
.type('application/xml')
},
payload: {
parse: false,
allow: 'application/xml'
}
}
}

return [
extensionsRoute
]
}

module.exports = {
getRequestedExtensions,
getExtensionsWithUpdates,
getExtensionsResponse,
setup
}
6 changes: 4 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let mq = require('./mq')

// Read in the channel / platform releases meta-data
let releases = setup.readReleases('data')
let extensions = setup.readExtensions()

if (process.env.DEBUG) {
console.log(_.keys(releases))
Expand All @@ -37,7 +38,8 @@ mq.setup((sender) => {
}

// POST, DEL and GET /1/releases/{platform}/{version}
let routes = require('./controllers/releases').setup(runtime, releases)
let releaseRoutes = require('./controllers/releases').setup(runtime, releases)
let extensionRoutes = require('./controllers/extensions').setup(runtime, extensions)
let crashes = require('./controllers/crashes').setup(runtime)
let monitoring = require('./controllers/monitoring').setup(runtime)

Expand Down Expand Up @@ -80,7 +82,7 @@ mq.setup((sender) => {
server.route(
[
common.root
].concat(routes, crashes, monitoring)
].concat(releaseRoutes, extensionRoutes, crashes, monitoring)
)

server.start((err) => {
Expand Down
13 changes: 13 additions & 0 deletions src/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ exports.readReleases = (directory) => {

return releases
}

// I'm not sure how we'll organize this in the future, but for now just pass along static data
// Format is: [extensionId, version, hash]
exports.readExtensions = () => [
// 1Password
['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90', 'f75d7808766429ec63ec41d948c1cb6a486407945d604961c6adf54fe3f459b7'],
// PDFJS
['oemmndcbldboiebfnladdacbdfmadadm', '1.5.294', '499e05d5cde9a1e735e29fa49af7839690f34eb27a3d952b8e4396ea50c77526'],
// Dashlane
['fdjamakpfbbddfjaooikfcpapjohcfmg', '4.2.4', '0be29a787290db4c554fd7c77e5c45939d2161688b6cb6b51d39cdedb9cc69d4'],
// LastPass
['hdokiejnpimakedhajhdlcegeplioahd', '4.1.28', '1e94a15dfaa59afd8ceb8b8cace7194aea3cc718d9a77fcff812eac918246e80']
]
99 changes: 99 additions & 0 deletions test/extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
var tap = require('tap')

var {getRequestedExtensions, getExtensionsWithUpdates} = require('../dist/controllers/extensions')

const request = (appId) => (version) => `
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="chrome-53.0.2785.116" prodversion="53.0.2785.116" requestid="{b4f77b70-af29-462b-a637-8a3e4be5ecd9}" lang="" updaterchannel="stable" prodchannel="stable" os="mac" arch="x64" nacl_arch="x86-64">
<hw physmemory="16"/>
<os platform="Mac OS X" version="10.11.6" arch="x86_64"/>
<app appid="${appId}" version="${version}" installsource="ondemand">
<updatecheck />
<ping rd="-2" ping_freshness="" />
</app>
</request>`
const onePasswordRequest = request('aomjjhallfgjeglblehebfpbcfeobpgk')
const unknownExtensionRequest = request('this-is-a-fake-id')
const onePasswordAndPDFJSRequest = (onePasswordVersion, pdfJSVersion) => `
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="chrome-53.0.2785.116" prodversion="53.0.2785.116" requestid="{b4f77b70-af29-462b-a637-8a3e4be5ecd9}" lang="" updaterchannel="stable" prodchannel="stable" os="mac" arch="x64" nacl_arch="x86-64">
<hw physmemory="16"/>
<os platform="Mac OS X" version="10.11.6" arch="x86_64"/>
<app appid="aomjjhallfgjeglblehebfpbcfeobpgk" version="${onePasswordVersion}" installsource="ondemand">
<updatecheck />
<ping rd="-2" ping_freshness="" />
</app>
<app appid="oemmndcbldboiebfnladdacbdfmadadm" version="${pdfJSVersion}" installsource="ondemand">
<updatecheck />
<ping rd="-2" ping_freshness="" />
</app>
</request>`
const noUpdatesRequest = `
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="chrome-53.0.2785.116" prodversion="53.0.2785.116" requestid="{b4f77b70-af29-462b-a637-8a3e4be5ecd9}" lang="" updaterchannel="stable" prodchannel="stable" os="mac" arch="x64" nacl_arch="x86-64">
<hw physmemory="16"/>
<os platform="Mac OS X" version="10.11.6" arch="x86_64"/>
</request>`
const unsupportedProtocolRequest = `
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="2.0" version="chrome-53.0.2785.116" prodversion="53.0.2785.116" requestid="{b4f77b70-af29-462b-a637-8a3e4be5ecd9}" lang="" updaterchannel="stable" prodchannel="stable" os="mac" arch="x64" nacl_arch="x86-64">
<app appid="aomjjhallfgjeglblehebfpbcfeobpgk">
<updatecheck codebase="https://s3.amazonaws.com/brave-extensions/release/aomjjhallfgjeglblehebfpbcfeobpgk/extension_4_5_9_90.crx" version="4.5.9.90"/>
</app>
</request>`

const availableExtensions = [
['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90', 'f75d7808766429ec63ec41d948c1cb6a486407945d604961c6adf54fe3f459b7'],
// PDFJS
['oemmndcbldboiebfnladdacbdfmadadm', '1.5.294', '499e05d5cde9a1e735e29fa49af7839690f34eb27a3d952b8e4396ea50c77526'],
// Dashlane
['fdjamakpfbbddfjaooikfcpapjohcfmg', '4.2.4', '0be29a787290db4c554fd7c77e5c45939d2161688b6cb6b51d39cdedb9cc69d4'],
// LastPass
['hdokiejnpimakedhajhdlcegeplioahd', '4.1.28', '1e94a15dfaa59afd8ceb8b8cace7194aea3cc718d9a77fcff812eac918246e80']
]

tap.test('Extracts extension information from requests', (test) => {
tap.same(getRequestedExtensions(onePasswordRequest('0.0.0.0')), [['aomjjhallfgjeglblehebfpbcfeobpgk', '0.0.0.0']])
tap.same(getRequestedExtensions(onePasswordRequest('4.5.9.90')), [['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90']])
tap.same(getRequestedExtensions(onePasswordAndPDFJSRequest('4.5.9.90', '1.5.294')), [['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90'], ['oemmndcbldboiebfnladdacbdfmadadm', '1.5.294']])
tap.equal(getRequestedExtensions(unsupportedProtocolRequest), undefined)
test.end()
})

tap.test('Initial update for an extension works', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(onePasswordRequest('0.0.0.0'))),
[
['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90', 'f75d7808766429ec63ec41d948c1cb6a486407945d604961c6adf54fe3f459b7']
])
test.end()
})

tap.test('No updates returned for same version', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(onePasswordRequest('4.5.9.90'))), [])
test.end()
})

tap.test('No updates returned for unknown extension ID', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(unknownExtensionRequest('0.0.0.0'))), [])
test.end()
})

tap.test('No updates returned for newer extension ID', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(onePasswordRequest('9.5.9.90'))), [])
test.end()
})

tap.test('Blank update request returns no updates', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(noUpdatesRequest)), [])
test.end()
})

tap.test('Update for multiple extensions works', (test) => {
tap.same(getExtensionsWithUpdates(availableExtensions, getRequestedExtensions(onePasswordAndPDFJSRequest('0.0.0.0', '0.0.0.0'))),
[
['aomjjhallfgjeglblehebfpbcfeobpgk', '4.5.9.90', 'f75d7808766429ec63ec41d948c1cb6a486407945d604961c6adf54fe3f459b7'],
['oemmndcbldboiebfnladdacbdfmadadm', '1.5.294', '499e05d5cde9a1e735e29fa49af7839690f34eb27a3d952b8e4396ea50c77526']
])
test.end()
})

0 comments on commit 8838740

Please sign in to comment.