Skip to content

Commit

Permalink
My basic idea, moved from audit_couchdb
Browse files Browse the repository at this point in the history
  • Loading branch information
jhs committed Mar 4, 2011
1 parent 8c2c667 commit b9c99a8
Show file tree
Hide file tree
Showing 8 changed files with 506 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// The probe_couchdb API
//

module.exports = { "CouchDB" : require('./couch').CouchDB
, "Database" : require('./db').Database
, "DesignDocument": require('./ddoc').DesignDocument
}
69 changes: 69 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
// The probe_couchdb command-line interface.
//

var fs = require('fs')
, assert = require('assert')
, probe_couchdb = require('./api')
;

function usage() {
console.log([ 'usage: probe_couchdb <URL>'
, ''
].join("\n"));
}

var couch_url = process.argv[2];
if(!couch_url) {
usage();
process.exit(1);
}

if(!/^https?:\/\//.test(couch_url))
couch_url = 'http://' + couch_url;

var couch = new probe_couchdb.CouchDB();
couch.url = couch_url;
couch.log.setLevel('debug');

var count = 0;
function line() {
count += 1;
var parts = [count].concat(Array.prototype.slice.apply(arguments));
console.log(parts.join("\t"));
}

function handler_for(ev_name) {
return function event_handler(obj) {
line(ev_name, JSON.stringify(obj));
}
}

; ['couchdb', 'dbs', 'session', 'config'].forEach(function(ev_name) {
couch.on(ev_name, handler_for(ev_name));
})

couch.on('users', function show_users(users) {
line('users', '(' + users.length + ' users, including the anonymous user)');
})

couch.on('db', function(db) {
; ['metadata', 'security', 'ddoc_ids'].forEach(function(ev_name) {
db.on(ev_name, handler_for([ev_name, db.name].join(':')));
})

db.on('ddoc', function(ddoc) {
var path = [db.name, ddoc.id].join('/');

; ['info'].forEach(function(ev_name) {
ddoc.on(ev_name, handler_for([ev_name, path].join(':')));
})

ddoc.on('body', function show_ddoc_body(body) {
line(['body', path].join(':'), '(' + JSON.stringify(body).length + ' characters; ' + Object.keys(body).length + ' top-level keys)');
})
})
})

line("Number", "Event", "Data");
couch.start();
140 changes: 140 additions & 0 deletions couch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Probe all details about a CouchDB.
//

var lib = require('./lib')
, util = require('util')
, Emitter = require('./emitter').Emitter
, Database = require('./db').Database
;

var MAX_USER_DEFAULT = 1000;

function CouchDB () {
var self = this;
Emitter.call(self);

self.url = null;
self.only_dbs = null;
self.max_users = MAX_USER_DEFAULT;

self.on('start', function ping_root() {
self.log.debug("Pinging: " + self.url);
self.request({uri:self.url}, function(er, resp, body) {
if(er)
throw er;
else if(resp.statusCode !== 200 || body.couchdb !== "Welcome")
throw new Error("Bad welcome from " + self.url + ": " + JSON.stringify(body));
else
self.x_emit('couchdb', body);
})
})

self.on('couchdb', function probe_databases(hello) {
var all_dbs = lib.join(self.url, '/_all_dbs');
self.log.debug("Scanning databases: " + all_dbs);
self.request({uri:all_dbs}, function(er, resp, body) {
if(er) throw er;
if(resp.statusCode !== 200 || !Array.isArray(body))
throw new Error("Bad _all_dbs from " + all_dbs + ": " + JSON.stringify(body));

self.log.debug(self.url + ' has ' + body.length + ' databases');
var dbs = body.filter(function(db) { return !self.only_dbs || self.only_dbs.indexOf(db) !== -1 });
self.x_emit('dbs', dbs);
})
})

self.on('dbs', function emit_db_probes(dbs) {
self.log.debug('Creating probes for ' + dbs.length + ' dbs');
dbs.forEach(function(db_name) {
var db = new Database;
db.couch = self.url;
db.name = db_name;
self.x_emit('db', db);
db.start();
})
})

self.on('couchdb', function probe_session(hello) {
var session_url = lib.join(self.url, '/_session');
self.log.debug("Checking login session: " + session_url);
self.request({uri:session_url}, function(er, resp, session) {
if(er) throw er;
if(resp.statusCode !== 200 || (!session) || session.ok !== true)
throw new Error("Bad _session from " + session_url + ": " + JSON.stringify(session));

self.log.debug("Received session: " + JSON.stringify(session));
if( ((session.userCtx || {}).roles || []).indexOf('_admin') === -1 )
self.log.warn("Results will be incomplete without _admin access");
self.x_emit('session', session);
})
})

self.on('couchdb', function(hello) {
var config_url = lib.join(self.url, '/_config');
self.log.debug("Checking config: " + config_url);
self.request({uri:config_url}, function(er, resp, config) {
if(er) throw er;
if(resp.statusCode !== 200 || (typeof config !== 'object')) {
self.log.debug("Bad config response: " + JSON.stringify(config));
config = null;
}
self.x_emit('config', config);
})
})

self.on('config', function(config) {
// Once the config is known, the list of users can be established.
var auth_db = config && config.couch_httpd_auth && config.couch_httpd_auth.authentication_db;
if(!auth_db) {
auth_db = '_users';
self.log.warn('authentication_db not found in config; trying ' + JSON.stringify(auth_db));
}

// Of course, the anonymous user is always known to exist.
var anonymous_users = [ { name:null, roles: [] } ];

var auth_db_url = lib.join(self.url, encodeURIComponent(auth_db).replace(/^_design%2[fF]/, '_design/'));
self.log.debug("Checking auth_db: " + auth_db_url);
self.request({uri:auth_db_url}, function(er, resp, body) {
if(er) throw er;
if(resp.statusCode !== 200 || typeof config !== 'object') {
self.log.warn("Can not access authentication_db: " + auth_db_url);
// Signal the end of the users discovery.
self.x_emit('users', anonymous_users);
} else if(body.doc_count > self.max_users) {
throw new Error("Too many users; you must add a view to process them");
// TODO
} else {
var users_query = lib.join(auth_db_url, '/_all_docs'
+ '?include_docs=true'
+ '&startkey=' + encodeURIComponent(JSON.stringify("org.couchdb.user:"))
+ '&endkey=' + encodeURIComponent(JSON.stringify("org.couchdb.user;"))
);
self.log.debug("Fetching all users: " + users_query);
self.request({uri:users_query}, function(er, resp, body) {
if(er) throw er;
if(resp.statusCode !== 200 || !Array.isArray(body.rows))
throw new Error("Failed to fetch user listing from " + users_query + ": " + JSON.stringify(body));

var users = body.rows.map(function(row) { return row.doc });
self.log.debug("Found " + (users.length+1) + " users (including anonymous): " + auth_db_url);
self.x_emit('users', anonymous_users.concat(users));
})
}
})
})
} // CouchDB

util.inherits(CouchDB, Emitter);

CouchDB.prototype.start = function() {
var self = this;

if(!self.url)
throw new Error("url required");

self.x_emit('start');
}

module.exports = { "CouchDB": CouchDB
};
98 changes: 98 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Probe all details about a CouchDB database.
//

var lib = require('./lib')
, util = require('util')
, assert = require('assert')
, Emitter = require('./emitter').Emitter
, DesignDocument = require('./ddoc').DesignDocument
;

function Database () {
var self = this;
Emitter.call(self);

self.couch = null;
self.name = null;
self.url = null;

self.on('start', function probe_metadata() {
self.log.debug("Fetching db metadata: " + self.url);
self.request({uri:self.url}, function(er, resp, info) {
if(er)
throw er;
else if(resp.statusCode === 401 && typeof info === 'object' && info.error === 'unauthorized')
// Indicate no read permission.
self.x_emit('metadata', null);
else if(resp.statusCode === 200 && typeof info === 'object')
self.x_emit('metadata', info);
else
throw new Error("Unknown db responses: " + JSON.stringify(info));
})
})

self.on('start', function probe_security_object() {
var sec_url = lib.join(self.url, '/_security');
self.log.debug("Fetching db security data: " + sec_url);
self.request({uri:sec_url}, function(er, resp, security) {
if(er)
throw er;
else if(resp.statusCode === 401 && typeof security === 'object' && security.error === 'unauthorized')
// Indicate no read permission.
self.x_emit('security', null);
else if(resp.statusCode === 200 && typeof security === 'object')
self.x_emit('security', security);
else
throw new Error("Unknown db responses: " + JSON.stringify(security));
})
})

self.on('start', function probe_ddocs() {
var view = lib.join(self.url, '/_all_docs'
+ '?include_docs=false'
+ '&startkey=' + encodeURIComponent(JSON.stringify("_design/"))
+ '&endkey=' + encodeURIComponent(JSON.stringify("_design0"))
);
self.log.debug("Scanning for design documents: " + self.name);
self.request({uri:view}, function(er, resp, body) {
if(er)
throw er;
else if(resp.statusCode === 401 && typeof body === 'object' && body.error === 'unauthorized') {
// Indicate no read permisssion.
self.x_emit('ddoc_ids', null);
} else if(resp.statusCode === 200 && ("rows" in body)) {
var ids = body.rows.map(function(row) { return row.id });
self.log.debug(self.name + ' has ' + ids.length + ' design documents: ' + ids.join(', '));
self.x_emit('ddoc_ids', ids);
} else
throw new Error("Bad ddoc response from " + view + ": " + JSON.stringify({code:resp.statusCode, body:body}));
})
})

self.on('ddoc_ids', function emit_ddoc_probes(ids) {
self.log.debug('Creating probes for ' + ids.length + ' ddocs');
ids.forEach(function(id) {
var ddoc = new DesignDocument;
ddoc.db = self.url;
ddoc.id = id;
self.x_emit('ddoc', ddoc);
ddoc.start();
})
})
} // Database
util.inherits(Database, Emitter);

Database.prototype.start = function() {
var self = this;

if(!self.couch)
throw new Error("Couch URL required");
if(!self.name)
throw new Error("Database name required");

self.url = lib.join(self.couch, encodeURIComponent(self.name));
self.x_emit('start');
}

module.exports = { "Database": Database
};
60 changes: 60 additions & 0 deletions ddoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Probe all details about a CouchDB design document
//

var lib = require('./lib')
, util = require('util')
, assert = require('assert')
, Emitter = require('./emitter').Emitter
;

function DesignDocument () {
var self = this;
Emitter.call(self);

self.db = null;
self.id = null;
self.url = null;

self.on('start', function probe_doc() {
self.log.debug("Fetching design document: " + self.url);
self.request({uri:self.url}, function(er, resp, body) {
if(er)
throw er;
else if(resp.statusCode !== 200 || typeof body !== 'object')
throw new Error("Bad ddoc response from " + self.url + ": " + JSON.stringify({code:resp.statusCode, body:body}));

self.x_emit('body', body);
})
})

self.on('start', function probe_info() {
var info_url = lib.join(self.url, '/_info');
self.log.debug("Fetching ddoc info: " + info_url);
self.request({uri:info_url}, function(er, resp, body) {
if(er)
throw er;
else if(resp.statusCode !== 200 || typeof body !== 'object')
throw new Error("Bad ddoc response from " + info_url + ": " + JSON.stringify({code:resp.statusCode, body:body}));

self.x_emit('info', body);
})
})

} // DesignDocument
util.inherits(DesignDocument, Emitter);

DesignDocument.prototype.start = function() {
var self = this;

if(!self.db)
throw new Error("Couch database URL required");
if(!self.id)
throw new Error("Document ID required");

// Since this is a design document, the slash must be kept.
self.url = lib.join(self.db, encodeURIComponent(self.id).replace(/^_design%2[fF]/, '_design/'));
self.x_emit('start');
}

module.exports = { "DesignDocument": DesignDocument
};
Loading

0 comments on commit b9c99a8

Please sign in to comment.