diff options
author | Antonin Kral <a.kral@bobek.cz> | 2010-01-31 08:32:52 +0100 |
---|---|---|
committer | Antonin Kral <a.kral@bobek.cz> | 2010-01-31 08:32:52 +0100 |
commit | 4eefaf421bfeddf040d96a3dafb12e09673423d7 (patch) | |
tree | cb2e5ccc7f98158894f977ff131949da36673591 /db/dbwebserver.cpp | |
download | mongodb-4eefaf421bfeddf040d96a3dafb12e09673423d7.tar.gz |
Imported Upstream version 1.3.1
Diffstat (limited to 'db/dbwebserver.cpp')
-rw-r--r-- | db/dbwebserver.cpp | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/db/dbwebserver.cpp b/db/dbwebserver.cpp new file mode 100644 index 0000000..0e1483c --- /dev/null +++ b/db/dbwebserver.cpp @@ -0,0 +1,499 @@ +/* dbwebserver.cpp + + This is the administrative web page displayed on port 28017. +*/ + +/** +* Copyright (C) 2008 10gen Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "stdafx.h" +#include "../util/miniwebserver.h" +#include "../util/md5.hpp" +#include "db.h" +#include "repl.h" +#include "replset.h" +#include "instance.h" +#include "security.h" + +#include <pcrecpp.h> +#include <boost/date_time/posix_time/posix_time.hpp> +#undef assert +#define assert xassert + +namespace mongo { + + extern string bind_ip; + extern const char *replInfo; + + bool getInitialSyncCompleted(); + + time_t started = time(0); + + /* + string toString() { + stringstream ss; + unsigned long long dt = last - start; + ss << dt/1000; + ss << '\t'; + ss << timeLocked/1000 << '\t'; + if( dt ) + ss << (timeLocked*100)/dt << '%'; + return ss.str(); + } + */ + + struct Timing { + Timing() { + start = timeLocked = 0; + } + unsigned long long start, timeLocked; + }; + Timing tlast; + const int NStats = 32; + string lockStats[NStats]; + unsigned q = 0; + + void statsThread() { + /*cout << "TEMP disabled statsthread" << endl; + if( 1 ) + return;*/ + Client::initThread("stats"); + unsigned long long timeLastPass = 0; + while ( 1 ) { + { + /* todo: do we even need readlock here? if so for what? */ + readlock lk(""); + Top::completeSnapshot(); + q = (q+1)%NStats; + Timing timing; + dbMutex.info().getTimingInfo(timing.start, timing.timeLocked); + unsigned long long now = curTimeMicros64(); + if ( timeLastPass ) { + unsigned long long dt = now - timeLastPass; + unsigned long long dlocked = timing.timeLocked - tlast.timeLocked; + { + stringstream ss; + ss << dt / 1000 << '\t'; + ss << dlocked / 1000 << '\t'; + if ( dt ) + ss << (dlocked*100)/dt << '%'; + string s = ss.str(); + if ( cmdLine.cpu ) + log() << "cpu: " << s << endl; + lockStats[q] = s; + ClientCursor::idleTimeReport( (unsigned) ((dt - dlocked)/1000) ); + } + } + timeLastPass = now; + tlast = timing; + } + sleepsecs(4); + } + } + + bool _bold; + string bold(bool x) { + _bold = x; + return x ? "<b>" : ""; + } + string bold() { + return _bold ? "</b>" : ""; + } + + class DbWebServer : public MiniWebServer { + public: + // caller locks + void doLockedStuff(stringstream& ss) { + ss << "# databases: " << dbHolder.size() << '\n'; + if ( cc().database() ) { + ss << "curclient: " << cc().database()->name; // TODO: isn't this useless? + ss << '\n'; + } + ss << bold(ClientCursor::byLocSize()>10000) << "Cursors byLoc.size(): " << ClientCursor::byLocSize() << bold() << '\n'; + ss << "\n<b>replication</b>\n"; + ss << "master: " << master << '\n'; + ss << "slave: " << slave << '\n'; + if ( replPair ) { + ss << "replpair:\n"; + ss << replPair->getInfo(); + } + bool seemCaughtUp = getInitialSyncCompleted(); + if ( !seemCaughtUp ) ss << "<b>"; + ss << "initialSyncCompleted: " << seemCaughtUp; + if ( !seemCaughtUp ) ss << "</b>"; + ss << '\n'; + + ss << "\n<b>DBTOP</b>\n"; + ss << "<table border=1><tr align='left'><th>Namespace</th><th>%</th><th>Reads</th><th>Writes</th><th>Calls</th><th>Time</th>"; + vector< Top::Usage > usage; + Top::usage( usage ); + for( vector< Top::Usage >::iterator i = usage.begin(); i != usage.end(); ++i ) + ss << setprecision( 2 ) << fixed << "<tr><td>" << i->ns << "</td><td>" << i->pct << "</td><td>" + << i->reads << "</td><td>" << i->writes << "</td><td>" << i->calls << "</td><td>" << i->time << "</td></tr>\n"; + ss << "</table>"; + + ss << "\n<b>dt\ttlocked</b>\n"; + unsigned i = q; + while ( 1 ) { + ss << lockStats[i] << '\n'; + i = (i-1)%NStats; + if ( i == q ) + break; + } + } + + void doUnlockedStuff(stringstream& ss) { + /* this is in the header already ss << "port: " << port << '\n'; */ + ss << mongodVersion() << "\n"; + ss << "git hash: " << gitVersion() << "\n"; + ss << "sys info: " << sysInfo() << "\n"; + ss << "\n"; + ss << "dbwritelocked: " << dbMutex.info().isLocked() << " (initial)\n"; + ss << "uptime: " << time(0)-started << " seconds\n"; + if ( replAllDead ) + ss << "<b>replication replAllDead=" << replAllDead << "</b>\n"; + ss << "\nassertions:\n"; + for ( int i = 0; i < 4; i++ ) { + if ( lastAssert[i].isSet() ) { + ss << "<b>"; + if ( i == 3 ) ss << "usererr"; + else ss << i; + ss << "</b>" << ' ' << lastAssert[i].toString(); + } + } + + ss << "\nreplInfo: " << replInfo << "\n\n"; + + ss << "Clients:\n"; + ss << "<table border=1><tr align='left'><th>Thread</th><th>Current op</th>\n"; + { + boostlock bl(Client::clientsMutex); + for( set<Client*>::iterator i = Client::clients.begin(); i != Client::clients.end(); i++ ) { + Client *c = *i; + CurOp& co = *(c->curop()); + ss << "<tr><td>" << c->desc() << "</td><td"; + BSONObj info = co.infoNoauth(); + /* + if( info.getIntField("inLock") > 0 ) + ss << "style='color:red'"; + else if( info.getIntField("inLock") < 0 ) + ss << "style='color:green'"; + */ + ss << ">" << info << "</td></tr>\n"; + } + } + ss << "</table>\n"; + } + + bool allowed( const char * rq , vector<string>& headers, const SockAddr &from ){ + + if ( from.localhost() ) + return true; + + if ( db.findOne( "admin.system.users" , BSONObj() ).isEmpty() ) + return true; + + string auth = getHeader( rq , "Authorization" ); + + if ( auth.size() > 0 && auth.find( "Digest " ) == 0 ){ + auth = auth.substr( 7 ) + ", "; + + map<string,string> parms; + pcrecpp::StringPiece input( auth ); + + string name, val; + pcrecpp::RE re("(\\w+)=\"?(.*?)\"?, "); + while ( re.Consume( &input, &name, &val) ){ + parms[name] = val; + } + + BSONObj user = db.findOne( "admin.system.users" , BSON( "user" << parms["username"] ) ); + if ( ! user.isEmpty() ){ + string ha1 = user["pwd"].str(); + string ha2 = md5simpledigest( (string)"GET" + ":" + parms["uri"] ); + + string r = ha1 + ":" + parms["nonce"]; + if ( parms["nc"].size() && parms["cnonce"].size() && parms["qop"].size() ){ + r += ":"; + r += parms["nc"]; + r += ":"; + r += parms["cnonce"]; + r += ":"; + r += parms["qop"]; + } + r += ":"; + r += ha2; + r = md5simpledigest( r ); + + if ( r == parms["response"] ) + return true; + } + + + } + + stringstream authHeader; + authHeader + << "WWW-Authenticate: " + << "Digest realm=\"mongo\", " + << "nonce=\"abc\", " + << "algorithm=MD5, qop=\"auth\" " + ; + + headers.push_back( authHeader.str() ); + return 0; + } + + virtual void doRequest( + const char *rq, // the full request + string url, + // set these and return them: + string& responseMsg, + int& responseCode, + vector<string>& headers, // if completely empty, content-type: text/html will be added + const SockAddr &from + ) + { + //out() << "url [" << url << "]" << endl; + + if ( url.size() > 1 ) { + if ( ! allowed( rq , headers, from ) ){ + responseCode = 401; + responseMsg = "not allowed\n"; + return; + } + handleRESTRequest( rq , url , responseMsg , responseCode , headers ); + return; + } + + + responseCode = 200; + stringstream ss; + ss << "<html><head><title>"; + + string dbname; + { + stringstream z; + z << "mongodb " << getHostName() << ':' << mongo::cmdLine.port << ' '; + dbname = z.str(); + } + ss << dbname << "</title></head><body><h2>" << dbname << "</h2><p>\n<pre>"; + + doUnlockedStuff(ss); + + int n = 2000; + Timer t; + while ( 1 ) { + if ( !dbMutex.info().isLocked() ) { + { + readlock lk(""); + ss << "time to get dblock: " << t.millis() << "ms\n"; + doLockedStuff(ss); + } + break; + } + sleepmillis(1); + if ( --n < 0 ) { + ss << "\n<b>timed out getting dblock</b>\n"; + break; + } + } + + ss << "</pre></body></html>"; + responseMsg = ss.str(); + + // we want to return SavedContext from before the authentication was performed + if ( ! allowed( rq , headers, from ) ){ + responseCode = 401; + responseMsg = "not allowed\n"; + return; + } + } + + void handleRESTRequest( const char *rq, // the full request + string url, + string& responseMsg, + int& responseCode, + vector<string>& headers // if completely empty, content-type: text/html will be added + ) { + + string::size_type first = url.find( "/" , 1 ); + if ( first == string::npos ) { + responseCode = 400; + return; + } + + string method = parseMethod( rq ); + string dbname = url.substr( 1 , first - 1 ); + string coll = url.substr( first + 1 ); + string action = ""; + + map<string,string> params; + if ( coll.find( "?" ) != string::npos ) { + parseParams( params , coll.substr( coll.find( "?" ) + 1 ) ); + coll = coll.substr( 0 , coll.find( "?" ) ); + } + + string::size_type last = coll.find_last_of( "/" ); + if ( last == string::npos ) { + action = coll; + coll = "_defaultCollection"; + } + else { + action = coll.substr( last + 1 ); + coll = coll.substr( 0 , last ); + } + + for ( string::size_type i=0; i<coll.size(); i++ ) + if ( coll[i] == '/' ) + coll[i] = '.'; + + string fullns = dbname + "." + coll; + + headers.push_back( (string)"x-action: " + action ); + headers.push_back( (string)"x-ns: " + fullns ); + headers.push_back( "Content-Type: text/plain;charset=utf-8" ); + + stringstream ss; + + if ( method == "GET" ) { + responseCode = 200; + handleRESTQuery( fullns , action , params , responseCode , ss ); + } + else if ( method == "POST" ) { + responseCode = 201; + handlePost( fullns , body( rq ) , params , responseCode , ss ); + } + else { + responseCode = 400; + headers.push_back( "X_err: bad request" ); + ss << "don't know how to handle a [" << method << "]"; + out() << "don't know how to handle a [" << method << "]" << endl; + } + + responseMsg = ss.str(); + } + + void handleRESTQuery( string ns , string action , map<string,string> & params , int & responseCode , stringstream & out ) { + Timer t; + + int skip = _getOption( params["skip"] , 0 ); + int num = _getOption( params["limit"] , _getOption( params["count" ] , 1000 ) ); // count is old, limit is new + + int one = 0; + if ( params["one"].size() > 0 && tolower( params["one"][0] ) == 't' ) { + num = 1; + one = 1; + } + + BSONObjBuilder queryBuilder; + + for ( map<string,string>::iterator i = params.begin(); i != params.end(); i++ ) { + if ( ! i->first.find( "filter_" ) == 0 ) + continue; + + const char * field = i->first.substr( 7 ).c_str(); + const char * val = i->second.c_str(); + + char * temp; + + // TODO: this is how i guess if something is a number. pretty lame right now + double number = strtod( val , &temp ); + if ( temp != val ) + queryBuilder.append( field , number ); + else + queryBuilder.append( field , val ); + } + + BSONObj query = queryBuilder.obj(); + + auto_ptr<DBClientCursor> cursor = db.query( ns.c_str() , query, num , skip ); + + if ( one ) { + if ( cursor->more() ) { + BSONObj obj = cursor->next(); + out << obj.jsonString() << "\n"; + } + else { + responseCode = 404; + } + return; + } + + out << "{\n"; + out << " \"offset\" : " << skip << ",\n"; + out << " \"rows\": [\n"; + + int howMany = 0; + while ( cursor->more() ) { + if ( howMany++ ) + out << " ,\n"; + BSONObj obj = cursor->next(); + out << " " << obj.jsonString(); + + } + out << "\n ],\n\n"; + + out << " \"total_rows\" : " << howMany << " ,\n"; + out << " \"query\" : " << query.jsonString() << " ,\n"; + out << " \"millis\" : " << t.millis() << "\n"; + out << "}\n"; + } + + // TODO Generate id and revision per couch POST spec + void handlePost( string ns, const char *body, map<string,string> & params, int & responseCode, stringstream & out ) { + try { + BSONObj obj = fromjson( body ); + db.insert( ns.c_str(), obj ); + } catch ( ... ) { + responseCode = 400; // Bad Request. Seems reasonable for now. + out << "{ \"ok\" : false }"; + return; + } + + responseCode = 201; + out << "{ \"ok\" : true }"; + } + + int _getOption( string val , int def ) { + if ( val.size() == 0 ) + return def; + return atoi( val.c_str() ); + } + + private: + static DBDirectClient db; + }; + + DBDirectClient DbWebServer::db; + + void webServerThread() { + boost::thread thr(statsThread); + Client::initThread("websvr"); + DbWebServer mini; + int p = cmdLine.port + 1000; + if ( mini.init(bind_ip, p) ) { + ListeningSockets::get()->add( mini.socket() ); + log() << "web admin interface listening on port " << p << endl; + mini.run(); + } + else { + log() << "warning: web admin interface failed to initialize on port " << p << endl; + } + cc().shutdown(); + } + +} // namespace mongo |