This commit is contained in:
Danny Coates 2014-04-13 18:27:20 -07:00
Коммит 28bd196ee1
14 изменённых файлов: 1185 добавлений и 0 удалений

1
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
node_modules

213
bin/db_patcher.js Normal file
Просмотреть файл

@ -0,0 +1,213 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var path = require('path')
var fs = require('fs')
var mysql = require('mysql')
var P = require('../promise.js')
var options = require('../config')
var log = require('../log')(options.logLevel, 'db-patcher')
var schemaDir = path.join(__dirname, '..', 'db', 'schema')
var patches = {}
var files = fs.readdirSync(schemaDir)
files.forEach(function(filename) {
var from, to
var m = filename.match(/^patch-(\d+)-(\d+)\.sql$/)
if (m) {
from = parseInt(m[1], 10)
to = parseInt(m[2], 10)
patches[from] = patches[from] || {}
patches[from][to] = fs.readFileSync(path.join(schemaDir, filename), { encoding: 'utf8'})
}
else {
console.warn("Startup error: Unknown file in schema/ directory - '%s'", filename)
process.exit(2)
}
})
// To run any patches we need to switch multipleStatements on
options.master.multipleStatements = true
// when creating the database, we need to connect without a database name
var database = options.master.database
delete options.master.database
var client = mysql.createConnection(options.master)
createDatabase()
.then(changeUser)
.then(checkDbMetadataExists)
.then(readDbPatchLevel)
.then(patchToRequiredLevel)
.then(closeAndReconnect)
.done(
function() {
log.info('Patching complete')
},
function(err) {
log.fatal(err)
process.exit(2)
}
)
// helper functions
function createDatabase() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:CreateDatabase' } )
client.query(
'CREATE DATABASE IF NOT EXISTS ' + database + ' CHARACTER SET utf8 COLLATE utf8_unicode_ci',
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:CreateDatabase', err: err.message })
return d.reject(err)
}
d.resolve()
}
)
return d.promise
}
function changeUser() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:ChangeUser' } )
client.changeUser(
{
user : options.master.user,
password : options.master.password,
database : database
},
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:ChangeUser', err: err.message })
return d.reject(err)
}
d.resolve()
}
)
return d.promise
}
function checkDbMetadataExists() {
log.trace( { op: 'MySql.createSchema:CheckDbMetadataExists' } )
var d = P.defer()
var query = "SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE table_schema = ? AND table_name = 'dbMetadata'"
client.query(
query,
[ database ],
function (err, result) {
if (err) {
log.trace( { op: 'MySql.createSchema:MakingTheSchema', err: err.message } )
return d.reject(err)
}
d.resolve(result[0].count === 0 ? false : true)
}
)
return d.promise
}
function readDbPatchLevel(dbMetadataExists) {
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel' } )
if ( dbMetadataExists === false ) {
// the table doesn't exist, so start at patch level 0
return P.resolve(0)
}
// find out what patch level the database is currently at
var d = P.defer()
var query = "SELECT value FROM dbMetadata WHERE name = ?"
client.query(
query,
[ options.patchKey ],
function(err, result) {
if (err) {
log.trace( { op: 'MySql.createSchema:ReadDbPatchLevel', err: err.message } )
return d.reject(err)
}
// convert the patch level from a string to a number
return d.resolve(+result[0].value)
}
)
return d.promise
}
function patchToRequiredLevel(currentPatchLevel) {
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel' } )
// if we don't need any patches
if ( options.patchLevel === currentPatchLevel ) {
log.trace( { op: 'MySql.createSchema:PatchToRequiredLevel', patch: 'No patch required' } )
return P.resolve()
}
// We don't want any reverse patches to be automatically applied, so
// just emit a warning and carry on.
if ( options.patchLevel < currentPatchLevel ) {
log.warn( { op: 'MySql.createSchema:PatchToRequiredLevel', err: 'Reverse patch required - must be done manually' } )
return P.resolve()
}
log.trace({
op: 'MySql.createSchema:PatchToRequiredLevel',
msg1: 'Patching from ' + currentPatchLevel + ' to ' + options.patchLevel
})
var promise = P.resolve()
var patchesToApply = []
// First, loop through all the patches we need to apply
// to make sure they exist.
while ( currentPatchLevel < options.patchLevel ) {
// check that this patch exists
if ( !patches[currentPatchLevel][currentPatchLevel+1] ) {
log.fatal({
op: 'MySql.createSchema:PatchToRequiredLevel',
err: 'Patch from level ' + currentPatchLevel + ' to ' + (currentPatchLevel+1) + ' does not exist'
});
process.exit(2)
}
patchesToApply.push({
sql : patches[currentPatchLevel][currentPatchLevel+1],
from : currentPatchLevel,
to : currentPatchLevel+1,
})
currentPatchLevel += 1
}
// now apply each patch
patchesToApply.forEach(function(patch) {
promise = promise.then(function() {
var d = P.defer()
log.trace({ op: 'MySql.createSchema:PatchToRequiredLevel', msg1: 'Updating DB for patch ' + patch.from + ' to ' + patch.to })
client.query(
patch.sql,
function(err) {
if (err) return d.reject(err)
d.resolve()
}
)
return d.promise
})
})
return promise
}
function closeAndReconnect() {
var d = P.defer()
log.trace( { op: 'MySql.createSchema:CloseAndReconnect' } )
client.end(
function (err) {
if (err) {
log.error({ op: 'MySql.createSchema:Closed', err: err.message })
return d.reject(err)
}
// create the mysql class
log.trace( { op: 'MySql.createSchema:ResolvingWithNewClient' } )
d.resolve('ok')
}
)
return d.promise
}

88
bin/db_server.js Normal file
Просмотреть файл

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var restify = require('restify')
var error = require('../error')
var bufferize = require('../bufferize')
var config = require('../config')
var log = require('../log')(config.logLevel, 'db-api')
var DB = require('../db/mysql')(log, error)
function startServer(db) {
function reply(fn) {
return function (req, res, next) {
log.trace(
{
op: 'request',
route: req.route.name,
id: req.params && req.params.id
}
)
fn.call(db, req.params.id, req.body)
.then(
function (result) {
log.info({ op: 'request.summary' })
if (Array.isArray(result)) {
res.send(result.map(bufferize.unbuffer))
}
else {
res.send(bufferize.unbuffer(result || {}))
}
},
function (err) {
log.error({ op: 'request.summary', err: err })
res.send(err.code || 500, err)
}
)
.done(next, next)
}
}
var api = restify.createServer()
api.use(restify.bodyParser())
api.use(bufferize.bufferizeRequest)
api.get('/account/:id', reply(db.account))
api.del('/account/:id', reply(db.deleteAccount))
api.put('/account/:id', reply(db.createAccount))
api.get('/account/:id/devices', reply(db.accountDevices))
api.post('/account/:id/reset', reply(db.resetAccount))
api.post('/account/:id/verifyEmail', reply(db.verifyEmail))
api.get('/sessionToken/:id', reply(db.sessionToken))
api.del('/sessionToken/:id', reply(db.deleteSessionToken))
api.put('/sessionToken/:id', reply(db.createSessionToken))
api.get('/keyFetchToken/:id', reply(db.keyFetchToken))
api.del('/keyFetchToken/:id', reply(db.deleteKeyFetchToken))
api.put('/keyFetchToken/:id', reply(db.createKeyFetchToken))
api.get('/accountResetToken/:id', reply(db.accountResetToken))
api.del('/accountResetToken/:id', reply(db.deleteAccountResetToken))
api.put('/accountResetToken/:id', reply(db.createAccountResetToken))
api.get('/passwordChangeToken/:id', reply(db.passwordChangeToken))
api.del('/passwordChangeToken/:id', reply(db.deletePasswordChangeToken))
api.put('/passwordChangeToken/:id', reply(db.createPasswordChangeToken))
api.get('/passwordForgotToken/:id', reply(db.passwordForgotToken))
api.del('/passwordForgotToken/:id', reply(db.deletePasswordForgotToken))
api.put('/passwordForgotToken/:id', reply(db.createPasswordForgotToken))
api.post('/passwordForgotToken/:id/update', reply(db.updatePasswordForgotToken))
api.post('/passwordForgotToken/:id/verified', reply(db.forgotPasswordVerified))
api.get('/emailRecord/:id', reply(db.emailRecord))
api.head('/emailRecord/:id', reply(db.accountExists))
api.get('/__heartbeat__', reply(db.ping))
api.listen(
config.port,
function () {
log.info({ op: 'listening', port: config.port })
}
)
}
DB.connect(config).done(startServer)

39
bufferize.js Normal file
Просмотреть файл

@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
function unbuffer(object) {
var keys = Object.keys(object)
for (var i = 0; i < keys.length; i++) {
var x = object[keys[i]]
if (Buffer.isBuffer(x)) {
object[keys[i]] = x.toString('hex')
}
}
return object
}
function bufferize(object) {
var keys = Object.keys(object)
for (var i = 0; i < keys.length; i++) {
var x = object[keys[i]]
if (typeof(x) === 'string' && HEX_STRING.test(x)) {
object[keys[i]] = Buffer(x, 'hex')
}
}
return object
}
function bufferizeRequest(req, res, next) {
if (req.body) { req.body = bufferize(req.body) }
if (req.params) { req.params = bufferize(req.params) }
next()
}
module.exports = {
unbuffer: unbuffer,
bufferize: bufferize,
bufferizeRequest: bufferizeRequest
}

33
config.js Normal file
Просмотреть файл

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
module.exports = require('rc')(
'fxa_db',
{
logLevel: 'trace',
port: 8000,
patchKey: 'schema-patch-level',
patchLevel: 2,
master: {
user: 'root',
password: '',
database: 'fxa',
host: '127.0.0.1',
port: 3306,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 100
},
slave: {
user: 'root',
password: '',
database: 'fxa',
host: '127.0.0.1',
port: 3306,
connectionLimit: 10,
waitForConnections: true,
queueLimit: 100
}
}
)

574
db/mysql.js Normal file
Просмотреть файл

@ -0,0 +1,574 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var mysql = require('mysql')
var P = require('../promise')
module.exports = function (log, error) {
// http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
var LOCK_ERRNOS = [ 1205, 1206, 1213, 1689 ]
// make a pool of connections that we can draw from
function MySql(options) {
// poolCluster will remove the pool after `removeNodeErrorCount` errors.
// We don't ever want to remove a pool because we only have one pool
// for writing and reading each. Connection errors are mostly out of our
// control for automatic recovery so monitoring of 503s is critical.
// Since `removeNodeErrorCount` is Infinity `canRetry` must be false
// to prevent inifinite retry attempts.
this.poolCluster = mysql.createPoolCluster(
{
removeNodeErrorCount: Infinity,
canRetry: false
}
)
// Use separate pools for master and slave connections.
this.poolCluster.add('MASTER', options.master)
this.poolCluster.add('SLAVE', options.slave)
this.getClusterConnection = P.promisify(this.poolCluster.getConnection, this.poolCluster)
this.statInterval = setInterval(
reportStats.bind(this),
options.statInterval || 15000
)
this.statInterval.unref()
}
function reportStats() {
var nodes = Object.keys(this.poolCluster._nodes).map(
function (name) {
return this.poolCluster._nodes[name]
}.bind(this)
)
var stats = nodes.reduce(
function (totals, node) {
totals.errors += node.errorCount
totals.connections += node.pool._allConnections.length
totals.queue += node.pool._connectionQueue.length
totals.free += node.pool._freeConnections.length
return totals
},
{
stat: 'mysql',
errors: 0,
connections: 0,
queue: 0,
free: 0
}
)
log.stat(stats)
}
// this will be called from outside this file
MySql.connect = function(options) {
// check that the database patch level is what we expect (or one above)
var mysql = new MySql(options)
return mysql.readOne("SELECT value FROM dbMetadata WHERE name = ?", options.patchKey)
.then(
function (result) {
var patchLevel = +result.value
if ( patchLevel !== options.patchLevel && patchLevel !== options.patchLevel + 1 ) {
throw new Error('dbIncorrectPatchLevel')
}
log.trace({
op: 'MySql.connect',
patchLevel: patchLevel,
patchLevelRequired: options.patchLevel
})
return mysql
}
)
}
MySql.prototype.close = function () {
this.poolCluster.end()
clearInterval(this.statInterval)
return P.resolve()
}
MySql.prototype.ping = function () {
return this.getConnection('MASTER')
.then(
function(connection) {
var d = P.defer()
connection.ping(
function (err) {
connection.release()
return err ? d.reject(err) : d.resolve()
}
)
return d.promise
}
)
}
// CREATE
var CREATE_ACCOUNT = 'INSERT INTO accounts' +
' (uid, normalizedEmail, email, emailCode, emailVerified, kA, wrapWrapKb,' +
' authSalt, verifierVersion, verifyHash, verifierSetAt, createdAt)' +
' VALUES (?, LOWER(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
MySql.prototype.createAccount = function (uid, data) {
data.normalizedEmail = data.email
data.createdAt = data.verifierSetAt = Date.now()
return this.write(
CREATE_ACCOUNT,
[
uid,
data.normalizedEmail,
data.email,
data.emailCode,
data.emailVerified,
data.kA,
data.wrapWrapKb,
data.authSalt,
data.verifierVersion,
data.verifyHash,
data.verifierSetAt,
data.createdAt
]
)
}
var CREATE_SESSION_TOKEN = 'INSERT INTO sessionTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createSessionToken = function (tokenId, sessionToken) {
return this.write(
CREATE_SESSION_TOKEN,
[
tokenId,
sessionToken.data,
sessionToken.uid,
sessionToken.createdAt
]
)
}
var CREATE_KEY_FETCH_TOKEN = 'INSERT INTO keyFetchTokens' +
' (tokenId, authKey, uid, keyBundle, createdAt)' +
' VALUES (?, ?, ?, ?, ?)'
MySql.prototype.createKeyFetchToken = function (tokenId, keyFetchToken) {
return this.write(
CREATE_KEY_FETCH_TOKEN,
[
tokenId,
keyFetchToken.authKey,
keyFetchToken.uid,
keyFetchToken.keyBundle,
keyFetchToken.createdAt
]
)
}
var CREATE_ACCOUNT_RESET_TOKEN = 'REPLACE INTO accountResetTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createAccountResetToken = function (tokenId, accountResetToken) {
return this.write(
CREATE_ACCOUNT_RESET_TOKEN,
[
tokenId,
accountResetToken.data,
accountResetToken.uid,
accountResetToken.createdAt
]
)
}
var CREATE_PASSWORD_FORGOT_TOKEN = 'REPLACE INTO passwordForgotTokens' +
' (tokenId, tokenData, uid, passCode, createdAt, tries)' +
' VALUES (?, ?, ?, ?, ?, ?)'
MySql.prototype.createPasswordForgotToken = function (tokenId, passwordForgotToken) {
return this.write(
CREATE_PASSWORD_FORGOT_TOKEN,
[
tokenId,
passwordForgotToken.data,
passwordForgotToken.uid,
passwordForgotToken.passCode,
passwordForgotToken.createdAt,
passwordForgotToken.tries
]
)
}
var CREATE_PASSWORD_CHANGE_TOKEN = 'REPLACE INTO passwordChangeTokens' +
' (tokenId, tokenData, uid, createdAt)' +
' VALUES (?, ?, ?, ?)'
MySql.prototype.createPasswordChangeToken = function (tokenId, passwordChangeToken) {
return this.write(
CREATE_PASSWORD_CHANGE_TOKEN,
[
tokenId,
passwordChangeToken.data,
passwordChangeToken.uid,
passwordChangeToken.createdAt
]
)
}
// READ
var ACCOUNT_EXISTS = 'SELECT uid FROM accounts WHERE normalizedEmail = LOWER(?)'
MySql.prototype.accountExists = function (email) {
return this.readOne(ACCOUNT_EXISTS, email)
}
var ACCOUNT_DEVICES = 'SELECT tokenId FROM sessionTokens WHERE uid = ?'
MySql.prototype.accountDevices = function (uid) {
return this.read(ACCOUNT_DEVICES, uid)
}
var SESSION_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
' a.emailVerified, a.email, a.emailCode, a.verifierSetAt' +
' FROM sessionTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.sessionToken = function (id) {
return this.readOne(SESSION_TOKEN, id)
}
var KEY_FETCH_TOKEN = 'SELECT t.authKey, t.uid, t.keyBundle, t.createdAt,' +
' a.emailVerified, a.verifierSetAt' +
' FROM keyFetchTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.keyFetchToken = function (id) {
return this.readOne(KEY_FETCH_TOKEN, id)
}
var ACCOUNT_RESET_TOKEN = 'SELECT t.uid, t.tokenData, t.createdAt,' +
' a.verifierSetAt' +
' FROM accountResetTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.accountResetToken = function (id) {
return this.readOne(ACCOUNT_RESET_TOKEN, id)
}
var PASSWORD_FORGOT_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt,' +
' t.passCode, t.tries, a.email, a.verifierSetAt' +
' FROM passwordForgotTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.passwordForgotToken = function (id) {
return this.readOne(PASSWORD_FORGOT_TOKEN, id)
}
var PASSWORD_CHANGE_TOKEN = 'SELECT t.tokenData, t.uid, t.createdAt, a.verifierSetAt' +
' FROM passwordChangeTokens t, accounts a' +
' WHERE t.tokenId = ? AND t.uid = a.uid'
MySql.prototype.passwordChangeToken = function (id) {
return this.readOne(PASSWORD_CHANGE_TOKEN, id)
}
var EMAIL_RECORD = 'SELECT uid, email, normalizedEmail, emailVerified, emailCode,' +
' kA, wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt' +
' FROM accounts' +
' WHERE normalizedEmail = LOWER(?)'
MySql.prototype.emailRecord = function (email) {
return this.readOne(EMAIL_RECORD, email)
}
var ACCOUNT = 'SELECT uid, email, normalizedEmail, emailCode, emailVerified, kA,' +
' wrapWrapKb, verifierVersion, verifyHash, authSalt, verifierSetAt, createdAt' +
' FROM accounts WHERE uid = ?'
MySql.prototype.account = function (uid) {
return this.readOne(ACCOUNT, uid)
}
// UPDATE
var UPDATE_PASSWORD_FORGOT_TOKEN = 'UPDATE passwordForgotTokens' +
' SET tries = ? WHERE tokenId = ?'
MySql.prototype.updatePasswordForgotToken = function (tokenId, token) {
return this.write(UPDATE_PASSWORD_FORGOT_TOKEN, [token.tries, tokenId])
}
// DELETE
MySql.prototype.deleteAccount = function (uid) {
return this.transaction(
function (connection) {
var tables = [
'sessionTokens',
'keyFetchTokens',
'accountResetTokens',
'passwordChangeTokens',
'passwordForgotTokens',
'accounts'
]
var queries = deleteFromTablesWhereUid(connection, tables, uid)
return P.all(queries)
}
)
}
var DELETE_SESSION_TOKEN = 'DELETE FROM sessionTokens WHERE tokenId = ?'
MySql.prototype.deleteSessionToken = function (tokenId) {
return this.write(DELETE_SESSION_TOKEN, [tokenId])
}
var DELETE_KEY_FETCH_TOKEN = 'DELETE FROM keyFetchTokens WHERE tokenId = ?'
MySql.prototype.deleteKeyFetchToken = function (tokenId) {
return this.write(DELETE_KEY_FETCH_TOKEN, [tokenId])
}
var DELETE_ACCOUNT_RESET_TOKEN = 'DELETE FROM accountResetTokens WHERE tokenId = ?'
MySql.prototype.deleteAccountResetToken = function (tokenId) {
return this.write(DELETE_ACCOUNT_RESET_TOKEN, [tokenId])
}
var DELETE_PASSWORD_FORGOT_TOKEN = 'DELETE FROM passwordForgotTokens WHERE tokenId = ?'
MySql.prototype.deletePasswordForgotToken = function (tokenId) {
return this.write(DELETE_PASSWORD_FORGOT_TOKEN, [tokenId])
}
var DELETE_PASSWORD_CHANGE_TOKEN = 'DELETE FROM passwordChangeTokens WHERE tokenId = ?'
MySql.prototype.deletePasswordChangeToken = function (tokenId) {
return this.write(DELETE_PASSWORD_CHANGE_TOKEN, [tokenId])
}
// BATCH
var RESET_ACCOUNT = 'UPDATE accounts' +
' SET verifyHash = ?, authSalt = ?, wrapWrapKb = ?, verifierSetAt = ?,' +
' verifierVersion = ?' +
' WHERE uid = ?'
MySql.prototype.resetAccount = function (uid, data) {
return this.transaction(
function (connection) {
var tables = [
'sessionTokens',
'keyFetchTokens',
'accountResetTokens',
'passwordChangeTokens',
'passwordForgotTokens'
]
var queries = deleteFromTablesWhereUid(connection, tables, uid)
queries.push(
query(
connection,
RESET_ACCOUNT,
[
data.verifyHash,
data.authSalt,
data.wrapWrapKb,
Date.now(),
data.verifierVersion,
uid
]
)
)
return P.all(queries)
}
)
}
var VERIFY_EMAIL = 'UPDATE accounts SET emailVerified = true WHERE uid = ?'
MySql.prototype.verifyEmail = function (uid) {
return this.write(VERIFY_EMAIL, [uid])
}
MySql.prototype.forgotPasswordVerified = function (tokenId, accountResetToken) {
return this.transaction(
function (connection) {
return P.all([
query(
connection,
DELETE_PASSWORD_FORGOT_TOKEN,
[tokenId]
),
query(
connection,
CREATE_ACCOUNT_RESET_TOKEN,
[
accountResetToken.tokenId,
accountResetToken.data,
accountResetToken.uid,
accountResetToken.createdAt
]
)
])
}
)
}
// Internal
MySql.prototype.singleQuery = function (poolName, sql, params) {
return this.getConnection(poolName)
.then(
function (connection) {
return query(connection, sql, params)
.then(
function (result) {
connection.release()
return result
},
function (err) {
connection.release()
throw err
}
)
}
)
}
MySql.prototype.transaction = function (fn) {
return retryable(
function () {
return this.getConnection('MASTER')
.then(
function (connection) {
return query(connection, 'BEGIN')
.then(
function () {
return fn(connection)
}
)
.then(
function (result) {
return query(connection, 'COMMIT')
.then(function () { return result })
}
)
.catch(
function (err) {
log.error({ op: 'MySql.transaction', err: err })
return query(connection, 'ROLLBACK')
.then(function () { throw err })
}
)
.then(
function (result) {
connection.release()
return result
},
function (err) {
connection.release()
throw err
}
)
}
)
}.bind(this),
LOCK_ERRNOS
)
.catch(
function (err) {
throw error.wrap(err)
}
)
}
MySql.prototype.readOne = function (sql, param) {
return this.read(sql, param).then(firstResult)
}
MySql.prototype.read = function (sql, param) {
return this.singleQuery('SLAVE*', sql, [param])
.catch(
function (err) {
log.error({ op: 'MySql.read', sql: sql, id: param, err: err })
throw error.wrap(err)
}
)
}
MySql.prototype.write = function (sql, params) {
return this.singleQuery('MASTER', sql, params)
.then(
function (result) {
log.trace({ op: 'MySql.write', sql: sql, result: result })
return {}
},
function (err) {
log.error({ op: 'MySql.write', sql: sql, err: err })
if (err.errno === 1062) {
err = error.duplicate()
}
else {
err = error.wrap(err)
}
throw err
}
)
}
MySql.prototype.getConnection = function (name) {
return retryable(
this.getClusterConnection,
[1040, 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET']
)
}
function firstResult(results) {
if (!results.length) { throw error.notFound() }
return results[0]
}
function query(connection, sql, params) {
var d = P.defer()
connection.query(
sql,
params || [],
function (err, results) {
if (err) { return d.reject(err) }
d.resolve(results)
}
)
return d.promise
}
function deleteFromTablesWhereUid(connection, tables, uid) {
return tables.map(
function (table) {
return query(connection, 'DELETE FROM ' + table + ' WHERE uid = ?', uid)
}
)
}
function retryable(fn, errnos) {
function success(result) {
return result
}
function failure(err) {
log.error({ op: 'MySql.retryable', err: err })
if (errnos.indexOf(err.errno) === -1) {
throw err
}
return fn()
}
return fn().then(success, failure)
}
return MySql
}

Просмотреть файл

@ -0,0 +1,9 @@
-- Create the 'dbMetadata' table.
-- Note: This should be the only thing in this initial patch.
CREATE TABLE dbMetadata (
name VARCHAR(255) NOT NULL PRIMARY KEY,
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1';

Просмотреть файл

@ -0,0 +1,2 @@
-- -- drop the dbMetadata table
-- DROP TABLE dbMetadata;

Просмотреть файл

@ -0,0 +1,59 @@
-- create all tables
CREATE TABLE IF NOT EXISTS accounts (
uid BINARY(16) PRIMARY KEY,
normalizedEmail VARCHAR(255) NOT NULL UNIQUE KEY,
email VARCHAR(255) NOT NULL,
emailCode BINARY(16) NOT NULL,
emailVerified BOOLEAN NOT NULL DEFAULT FALSE,
kA BINARY(32) NOT NULL,
wrapWrapKb BINARY(32) NOT NULL,
authSalt BINARY(32) NOT NULL,
verifyHash BINARY(32) NOT NULL,
verifierVersion TINYINT UNSIGNED NOT NULL,
verifierSetAt BIGINT UNSIGNED NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS sessionTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX session_uid (uid)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS keyFetchTokens (
tokenId BINARY(32) PRIMARY KEY,
authKey BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
keyBundle BINARY(96) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX key_uid (uid)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS accountResetTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL UNIQUE KEY,
createdAt BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS passwordForgotTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL UNIQUE KEY,
passCode BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
tries SMALLINT UNSIGNED NOT NULL
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS passwordChangeTokens (
tokenId BINARY(32) PRIMARY KEY,
tokenData BINARY(32) NOT NULL,
uid BINARY(16) NOT NULL,
createdAt BIGINT UNSIGNED NOT NULL,
INDEX session_uid (uid)
) ENGINE=InnoDB;
UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';

Просмотреть файл

@ -0,0 +1,10 @@
-- -- drop tables
-- DROP TABLE passwordChangeTokens;
-- DROP TABLE passwordForgotTokens;
-- DROP TABLE accountResetTokens;
-- DROP TABLE keyFetchTokens;
-- DROP TABLE sessionTokens;
-- DROP TABLE accounts;
-- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level';

51
error.js Normal file
Просмотреть файл

@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var inherits = require('util').inherits
function AppError(options) {
this.message = options.message
this.errno = options.errno
this.code = options.code
}
inherits(AppError, Error)
AppError.prototype.toString = function () {
return 'Error: ' + this.message
}
AppError.duplicate = function () {
return new AppError(
{
code: 409,
error: 'Conflict',
errno: 101,
message: 'Record already exists'
}
)
}
AppError.notFound = function () {
return new AppError(
{
code: 404,
error: 'Not Found',
errno: 116,
message: 'Not Found'
}
)
}
AppError.wrap = function (err) {
return new AppError(
{
code: 500,
error: 'Internal Server Error',
errno: err.errno,
message: err.code
}
)
}
module.exports = AppError

74
log.js Normal file
Просмотреть файл

@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var Domain = require('domain')
var util = require('util')
var Logger = require('bunyan')
var unbuffer = require('./bufferize').unbuffer
function Overdrive(options) {
Logger.call(this, options)
}
util.inherits(Overdrive, Logger)
Overdrive.prototype.trace = function () {
// TODO if this is a performance burden reintroduce the level check
// otherwise this is valuable data for debugging in the log.summary
var arg0 = arguments[0]
if (typeof(arg0) === 'object') {
unbuffer(arg0)
}
return Logger.prototype.trace.apply(this, arguments)
}
Overdrive.prototype.event = function (name, data) {
var e = {
event: name,
data: unbuffer(data)
}
process.stdout.write(JSON.stringify(e) + '\n')
}
Overdrive.prototype.stat = function (stats) {
stats.op = 'stat'
this.info(stats)
}
module.exports = function (level, name) {
var logStreams = [{ stream: process.stderr, level: level }]
name = name || 'fxa-auth-server'
var log = new Overdrive(
{
name: name,
streams: logStreams
}
)
process.stdout.on(
'error',
function (err) {
if (err.code === 'EPIPE') {
log.emit('error', err)
}
}
)
Object.keys(console).forEach(
function (key) {
console[key] = function () {
var json = { op: 'console', message: util.format.apply(null, arguments) }
if(log[key]) {
log[key](json)
}
else {
log.warn(json)
}
}
}
)
return log
}

27
package.json Normal file
Просмотреть файл

@ -0,0 +1,27 @@
{
"name": "fxa-auth-db-server",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "tap test",
"start": "node ./bin/db_server.js 2>&1 | bunyan -o short"
},
"repository": {
"type": "git",
"url": "https://github.com/dannycoates/fxa-auth-db-server"
},
"author": "",
"license": "MPL 2.0",
"bugs": {
"url": "https://github.com/dannycoates/fxa-auth-db-server/issues"
},
"homepage": "https://github.com/dannycoates/fxa-auth-db-server",
"dependencies": {
"restify": "2.7.0",
"mysql": "2.1.1",
"bluebird": "1.2.2",
"bunyan": "0.22.3",
"rc": "0.3.4"
}
}

5
promise.js Normal file
Просмотреть файл

@ -0,0 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
module.exports = require('bluebird')