/* 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 "pch.h" #include "../util/miniwebserver.h" #include "../util/web/html.h" #include "../util/md5.hpp" #include "db.h" #include "repl.h" #include "replpair.h" #include "instance.h" #include "security.h" #include "stats/snapshots.h" #include "background.h" #include "commands.h" #include #include #undef assert #define assert MONGO_assert namespace mongo { using namespace mongoutils::html; 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 ? "" : ""; } bool execCommand( Command * c , Client& client , int queryOptions , const char *ns, BSONObj& cmdObj , BSONObjBuilder& result, bool fromRepl ); class DbWebServer : public MiniWebServer { public: DbWebServer(const string& ip, int port) :MiniWebServer(ip, port) {} // caller locks void doLockedStuff(stringstream& ss) { ss << "# databases: " << dbHolder.size() << '\n'; if( ClientCursor::byLocSize()>500 ) ss << bold(ClientCursor::byLocSize()>10000) << "Cursors byLoc.size(): " << ClientCursor::byLocSize() << bold() << '\n'; ss << "\nreplication: "; if( *replInfo ) ss << "\nreplInfo: " << replInfo << "\n\n"; if( replSet ) { ss << a("", "see replSetGetStatus link top of page") << "--replSet " << cmdLine.replSet << '\n'; } else { ss << "\nmaster: " << 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 << "\n"; 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 << "
"; ss << a("http://www.mongodb.org/display/DOCS/Developer+FAQ#DeveloperFAQ-What%27sa%22namespace%22%3F", "namespace") << "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 << "\n"; } 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 << "uptime: " << time(0)-started << " seconds\n"; if ( replAllDead ) ss << "replication replAllDead=" << replAllDead << "\n"; ss << a("", "information on caught assertion exceptions"); ss << "assertions:\n"; for ( int i = 0; i < 4; i++ ) { if ( lastAssert[i].isSet() ) { if ( i == 3 ) ss << "uassert"; else if( i == 2 ) ss << "massert"; else if( i == 0 ) ss << "assert"; else if( i == 1 ) ss << "warnassert"; else ss << i; ss << ' ' << lastAssert[i].toString(); } } ss << "\n"; 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 << "\n"; } } ss << "
ClientOpIdActiveLockTypeWaitingSecsRunningOpNameSpaceQueryclientmsgprogress
" << c->desc() << "
\n"; } bool allowed( const char * rq , vector& headers, const SockAddr &from ){ if ( from.isLocalHost() ) 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 ) { if ( url.size() > 1 ) { if ( url.find( "/_status" ) == 0 ){ if ( ! allowed( rq , headers, from ) ){ responseCode = 401; headers.push_back( "Content-Type: text/plain" ); responseMsg = "not allowed\n"; return; } headers.push_back( "Content-Type: application/json" ); generateServerStatus( url , responseMsg ); responseCode = 200; return; } if ( ! cmdLine.rest ) { responseCode = 403; stringstream ss; ss << "REST is not enabled. use --rest to turn on.\n"; ss << "check that port " << _port << " is secured for the network too.\n"; responseMsg = ss.str(); headers.push_back( "Content-Type: text/plain" ); return; } if( url.find("/_commands") == 0 ) { if ( ! allowed( rq , headers, from ) ){ responseCode = 401; headers.push_back( "Content-Type: text/plain" ); responseMsg = "not allowed\n"; return; } headers.push_back( "Content-Type: text/html" ); stringstream ss; ss << "Commands List"; ss << p(a("/", "", "Back")); ss << p("MongoDB List of Commands\n"); const map *m = Command::commandsByBestName(); ss << "S:slave-only N:no-lock R:read-lock W:write-lock A:admin-only
\n"; ss << ""; ss << "\n"; for( map::const_iterator i = m->begin(); i != m->end(); i++ ) { i->second->htmlHelp(ss); } ss << "
CommandAttributesHelp
"; ss << ""; responseMsg = ss.str(); responseCode = 200; return; } /* run a command from the web ui */ const char *p = url.c_str(); if( *p == '/' ) { const char *h = strstr(p, "?text"); string cmd = p+1; if( h && h > p+1 ) cmd = string(p+1, h-p-1); const map *m = Command::webCommands(); if( m && m->count(cmd) ) { Command *c = m->find(cmd)->second; Client& client = cc(); BSONObjBuilder result; BSONObjBuilder b; b.append(c->name, 1); BSONObj cmdObj = b.obj(); execCommand(c, client, 0, "admin.", cmdObj, result, false); responseCode = 200; string j = result.done().jsonString(JS, h != 0 ? 1 : 0); if( h == 0 ) { headers.push_back( "Content-Type: application/json" ); } else { headers.push_back( "Content-Type: text/plain" ); } responseMsg = j; if( h ) responseMsg += '\n'; 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 << "mongod " << getHostName() << ":" << mongo::cmdLine.port; dbname = z.str(); } ss << dbname << "

" << dbname << "

\n"; ss << "List all commands\n"; ss << "
";
            //ss << "_status";
            {
                const map *m = Command::webCommands();
                if( m ) {
                    for( map::const_iterator i = m->begin(); i != m->end(); i++ ) { 
                        stringstream h;
                        i->second->help(h);
                        string help = h.str();
                        ss << "first << "?text\"";
                        if( help != "no help defined" )
                            ss << " title=\"" << help << '"';
                        ss << ">" << i->first << " ";
                    }
                    ss << '\n';
                }
            }
            ss << '\n';
            ss << "HTTP admin port:" << _port << "\n";

            doUnlockedStuff(ss);

            ss << "";
            ss << "write locked: " << (dbMutex.info().isLocked() ? "true" : "false") << "\n";
            {
                Timer t;
                readlocktry lk( "" , 2000 );
                if ( lk.got() ){
                    ss << "time to get readlock: " << t.millis() << "ms\n";
                    doLockedStuff(ss);
                }
                else {
                    ss << "\ntimed out getting dblock\n";
                }
            }
            

            ss << "
\n\n"; 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 ); uassert( 13085 , "query failed for dbwebserver" , cursor.get() ); 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"); const int p = cmdLine.port + 1000; DbWebServer mini(bind_ip, p); log() << "web admin interface listening on port " << p << endl; mini.initAndListen(); cc().shutdown(); } } // namespace mongo