/* 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 . */ #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 "stats/snapshots.h" #include "background.h" #include "commands.h" #include #include #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; }; bool _bold; string bold(bool x) { _bold = x; return x ? "" : ""; } string bold() { return _bold ? "" : ""; } class DbWebServer : public MiniWebServer { public: // caller locks void doLockedStuff(stringstream& ss) { ss << "# databases: " << dbHolder.size() << '\n'; ss << bold(ClientCursor::byLocSize()>10000) << "Cursors byLoc.size(): " << ClientCursor::byLocSize() << bold() << '\n'; ss << "\nreplication\n"; ss << "master: " << replSettings.master << '\n'; ss << "slave: " << replSettings.slave << '\n'; if ( replPair ) { ss << "replpair:\n"; ss << replPair->getInfo(); } bool seemCaughtUp = getInitialSyncCompleted(); if ( !seemCaughtUp ) ss << ""; ss << "initialSyncCompleted: " << seemCaughtUp; if ( !seemCaughtUp ) ss << ""; ss << '\n'; auto_ptr delta = statsSnapshots.computeDelta(); if ( delta.get() ){ ss << "\nDBTOP (occurences|percent of elapsed)\n"; ss << ""; ss << ""; ss << "" "" "" "" "" "" "" "" ""; ss << ""; display( ss , (double) delta->elapsed() , "GLOBAL" , delta->globalUsageDiff() ); Top::UsageMap usage = delta->collectionUsageDiff(); for ( Top::UsageMap::iterator i=usage.begin(); i != usage.end(); i++ ){ display( ss , (double) delta->elapsed() , i->first , i->second ); } ss << "
NStotalReadsWritesQueriesGetMoresInsertsUpdatesRemoves
"; } statsSnapshots.outputLockInfoHTML( ss ); BackgroundOperation::dump(ss); } void display( stringstream& ss , double elapsed , const Top::UsageData& usage ){ ss << ""; ss << usage.count; ss << ""; double per = 100 * ((double)usage.time)/elapsed; ss << setprecision(2) << fixed << per << "%"; ss << ""; } void display( stringstream& ss , double elapsed , const string& ns , const Top::CollectionData& data ){ if ( ns != "GLOBAL" && data.total.count == 0 ) return; ss << "" << ns << ""; display( ss , elapsed , data.total ); display( ss , elapsed , data.readLock ); display( ss , elapsed , data.writeLock ); display( ss , elapsed , data.queries ); display( ss , elapsed , data.getmore ); display( ss , elapsed , data.insert ); display( ss , elapsed , data.update ); display( ss , elapsed , data.remove ); ss << ""; } void tablecell( stringstream& ss , bool b ){ ss << "" << (b ? "X" : "") << ""; } template< typename T> void tablecell( stringstream& ss , const T& t ){ ss << "" << t << ""; } 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 << "replication replAllDead=" << replAllDead << "\n"; ss << "\nassertions:\n"; for ( int i = 0; i < 4; i++ ) { if ( lastAssert[i].isSet() ) { ss << ""; if ( i == 3 ) ss << "usererr"; else ss << i; ss << "" << ' ' << lastAssert[i].toString(); } } ss << "\nreplInfo: " << replInfo << "\n\n"; ss << "Clients:\n"; ss << ""; ss << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "" << "\n"; { scoped_lock bl(Client::clientsMutex); for( set::iterator i = Client::clients.begin(); i != Client::clients.end(); i++ ) { Client *c = *i; CurOp& co = *(c->curop()); ss << ""; tablecell( ss , co.opNum() ); tablecell( ss , co.active() ); tablecell( ss , co.getLockType() ); tablecell( ss , co.isWaitingForLock() ); if ( co.active() ) tablecell( ss , co.elapsedSeconds() ); else tablecell( ss , "" ); tablecell( ss , co.getOp() ); tablecell( ss , co.getNS() ); if ( co.haveQuery() ) tablecell( ss , co.query() ); else tablecell( ss , "" ); tablecell( ss , co.getRemoteString() ); tablecell( ss , co.getMessage() ); tablecell( ss , co.getProgressMeter().toString() ); ss << ""; } } ss << "
ThreadOpIdActiveLockTypeWaitingSecsRunningOpNameSpaceQueryclientmsgprogress
" << c->desc() << "
\n"; } bool allowed( const char * rq , vector& headers, const SockAddr &from ){ if ( from.localhost() ) return true; Client::GodScope gs; if ( db.findOne( "admin.system.users" , BSONObj() , 0 , QueryOption_SlaveOk ).isEmpty() ) return true; string auth = getHeader( rq , "Authorization" ); if ( auth.size() > 0 && auth.find( "Digest " ) == 0 ){ auth = auth.substr( 7 ) + ", "; map 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& headers, // if completely empty, content-type: text/html will be added const SockAddr &from ) { //out() << "url [" << url << "]" << endl; if ( url.size() > 1 ) { if ( url.find( "/_status" ) == 0 ){ if ( ! allowed( rq , headers, from ) ){ responseCode = 401; responseMsg = "not allowed\n"; return; } generateServerStatus( url , responseMsg ); responseCode = 200; return; } if ( ! cmdLine.rest ){ responseCode = 403; responseMsg = "rest is not enabled. use --rest to turn on"; return; } if ( ! allowed( rq , headers, from ) ){ responseCode = 401; responseMsg = "not allowed\n"; return; } handleRESTRequest( rq , url , responseMsg , responseCode , headers ); return; } responseCode = 200; stringstream ss; ss << ""; string dbname; { stringstream z; z << "mongodb " << getHostName() << ':' << mongo::cmdLine.port << ' '; dbname = z.str(); } ss << dbname << "

" << dbname << "

\n

";

            doUnlockedStuff(ss);

            {
                Timer t;
                readlocktry lk( "" , 2000 );
                if ( lk.got() ){
                    ss << "time to get dblock: " << t.millis() << "ms\n";
                    doLockedStuff(ss);
                }
                else {
                    ss << "\ntimed out getting dblock\n";
                }
            }
            

            ss << "
"; 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 generateServerStatus( string url , string& responseMsg ){ static vector commands; if ( commands.size() == 0 ){ commands.push_back( "serverStatus" ); commands.push_back( "buildinfo" ); } BSONObj params; if ( url.find( "?" ) != string::npos ) { parseParams( params , url.substr( url.find( "?" ) + 1 ) ); } BSONObjBuilder buf(1024); for ( unsigned i=0; ilocktype() == 0 ); BSONObj co; { BSONObjBuilder b; b.append( cmd.c_str() , 1 ); if ( cmd == "serverStatus" && params["repl"].type() ){ b.append( "repl" , atoi( params["repl"].valuestr() ) ); } co = b.obj(); } string errmsg; BSONObjBuilder sub; if ( ! c->run( "admin.$cmd" , co , errmsg , sub , false ) ) buf.append( cmd.c_str() , errmsg ); else buf.append( cmd.c_str() , sub.obj() ); } responseMsg = buf.obj().jsonString(); } void handleRESTRequest( const char *rq, // the full request string url, string& responseMsg, int& responseCode, vector& 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 = ""; BSONObj 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 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, BSONObj& 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( BSONElement e , int def ) { if ( e.isNumber() ) return e.numberInt(); if ( e.type() == String ) return atoi( e.valuestr() ); return def; } private: static DBDirectClient db; }; DBDirectClient DbWebServer::db; void webServerThread() { 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