/* 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 . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/pch.h" #include "mongo/db/dbwebserver.h" #include #include #include "mongo/base/init.h" #include "mongo/db/auth/authorization_manager.h" #include "mongo/db/auth/authorization_manager_global.h" #include "mongo/db/auth/authorization_session.h" #include "mongo/db/auth/privilege.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/auth/user.h" #include "mongo/db/background.h" #include "mongo/db/commands.h" #include "mongo/db/db.h" #include "mongo/db/instance.h" #include "mongo/db/stats/snapshots.h" #include "mongo/util/admin_access.h" #include "mongo/util/md5.hpp" #include "mongo/util/mongoutils/html.h" #include "mongo/util/net/miniwebserver.h" #include "mongo/util/ramlog.h" #include "mongo/util/version.h" namespace mongo { using namespace mongoutils::html; using namespace bson; struct Timing { Timing() { start = timeLocked = 0; } unsigned long long start, timeLocked; }; class DbWebServer : public MiniWebServer { public: DbWebServer(const string& ip, int port, const AdminAccess* webUsers) : MiniWebServer("admin web console", ip, port), _webUsers(webUsers) { WebStatusPlugin::initAll(); } private: const AdminAccess* _webUsers; // not owned here void doUnlockedStuff(stringstream& ss) { /* this is in the header already ss << "port: " << port << '\n'; */ ss << "
";
            ss << mongodVersion() << '\n';
            ss << "git hash: " << gitVersion() << '\n';
            ss << openSSLVersion("OpenSSL version: ", "\n");
            ss << "sys info: " << sysInfo() << '\n';
            ss << "uptime: " << time(0)-serverGlobalParams.started << " seconds\n";
            ss << "
"; } void _authorizePrincipal(const UserName& userName) { Status status = cc().getAuthorizationSession()->addAndAuthorizeUser(userName); uassertStatusOK(status); } bool allowed( const char * rq , vector& headers, const SockAddr &from ) { if ( from.isLocalHost() || !_webUsers->haveAdminUsers() ) { // TODO(spencer): should the above check use "&&" not "||"? Currently this is much // more permissive than the server's localhost auth bypass. cc().getAuthorizationSession()->grantInternalAuthorization(); 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+)=\"?(.*?)\"?,\\s*"); while ( re.Consume( &input, &name, &val) ) { parms[name] = val; } // Only users in the admin DB are visible by the webserver UserName userName(parms["username"], "admin"); User* user; AuthorizationManager& authzManager = cc().getAuthorizationSession()->getAuthorizationManager(); Status status = authzManager.acquireUser(userName, &user); if (!status.isOK()) { if (status.code() != ErrorCodes::UserNotFound) { uasserted(17051, status.reason()); } } else { uassert(17090, "External users don't have a password", !user->getCredentials().isExternal); string ha1 = user->getCredentials().password; authzManager.releaseUser(user); string ha2 = md5simpledigest( (string)"GET" + ":" + parms["uri"] ); stringstream r; 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; string r1 = md5simpledigest( r.str() ); if ( r1 == parms["response"] ) { _authorizePrincipal(userName); 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 ( ! allowed( rq , headers, from ) ) { responseCode = 401; headers.push_back( "Content-Type: text/plain;charset=utf-8" ); responseMsg = "not allowed\n"; return; } { BSONObj params; const size_t pos = url.find( "?" ); if ( pos != string::npos ) { MiniWebServer::parseParams( params , url.substr( pos + 1 ) ); url = url.substr(0, pos); } DbWebHandler * handler = DbWebHandler::findHandler( url ); if ( handler ) { if (handler->requiresREST(url) && !serverGlobalParams.rest) { _rejectREST( responseMsg , responseCode , headers ); } else { string callback = params.getStringField("jsonp"); uassert(13453, "server not started with --jsonp", callback.empty() || serverGlobalParams.jsonp); handler->handle( rq , url , params , responseMsg , responseCode , headers , from ); if (responseCode == 200 && !callback.empty()) { responseMsg = callback + '(' + responseMsg + ')'; } } return; } } if (!serverGlobalParams.rest) { _rejectREST( responseMsg , responseCode , headers ); return; } responseCode = 404; headers.push_back( "Content-Type: text/html;charset=utf-8" ); responseMsg = "unknown url\n"; return; } // generate home page if ( ! allowed( rq , headers, from ) ) { responseCode = 401; headers.push_back( "Content-Type: text/plain;charset=utf-8" ); responseMsg = "not allowed\n"; return; } responseCode = 200; stringstream ss; string dbname; { stringstream z; z << serverGlobalParams.binaryName << ' ' << prettyHostName(); dbname = z.str(); } ss << start(dbname) << h2(dbname); ss << "

List all commands | \n"; ss << "Replica set status

\n"; //ss << "_status"; { const map *m = Command::webCommands(); if( m ) { ss << a("", "These read-only context-less commands can be executed from the web interface. " "Results are json format, unless ?text=1 is appended in which case the result is output as text " "for easier human viewing", "Commands") << ": "; for( map::const_iterator i = m->begin(); i != m->end(); i++ ) { stringstream h; i->second->help(h); string help = h.str(); ss << "first << "?text=1\""; if( help != "no help defined" ) ss << " title=\"" << help << '"'; ss << ">" << i->first << " "; } ss << '\n'; } } ss << '\n'; /* ss << "HTTP admin port:" << _port << "

\n"; */ doUnlockedStuff(ss); WebStatusPlugin::runAll( ss ); ss << "\n"; responseMsg = ss.str(); headers.push_back( "Content-Type: text/html;charset=utf-8" ); } void _rejectREST( string& responseMsg , int& responseCode, vector& headers ) { 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;charset=utf-8" ); } }; // --- bool prisort( const Prioritizable * a , const Prioritizable * b ) { return a->priority() < b->priority(); } // -- status framework --- WebStatusPlugin::WebStatusPlugin( const string& secionName , double priority , const string& subheader ) : Prioritizable(priority), _name( secionName ) , _subHeading( subheader ) { if ( ! _plugins ) _plugins = new vector(); _plugins->push_back( this ); } void WebStatusPlugin::initAll() { if ( ! _plugins ) return; sort( _plugins->begin(), _plugins->end() , prisort ); for ( unsigned i=0; i<_plugins->size(); i++ ) (*_plugins)[i]->init(); } void WebStatusPlugin::runAll( stringstream& ss ) { if ( ! _plugins ) return; for ( unsigned i=0; i<_plugins->size(); i++ ) { WebStatusPlugin * p = (*_plugins)[i]; ss << "


\n" << "" << p->_name << ""; ss << " " << p->_subHeading; ss << "
\n"; p->run(ss); } } vector * WebStatusPlugin::_plugins = 0; // -- basic status plugins -- class LogPlugin : public WebStatusPlugin { public: LogPlugin() : WebStatusPlugin( "Log" , 100 ), _log(0) { _log = RamLog::get( "global" ); } virtual void init() {} virtual void run( stringstream& ss ) { _log->toHTML( ss ); } RamLog * _log; }; MONGO_INITIALIZER(WebStatusLogPlugin)(InitializerContext*) { if (serverGlobalParams.isHttpInterfaceEnabled) { new LogPlugin; } return Status::OK(); } // -- handler framework --- DbWebHandler::DbWebHandler( const string& name , double priority , bool requiresREST ) : Prioritizable(priority), _name(name) , _requiresREST(requiresREST) { { // setup strings _defaultUrl = "/"; _defaultUrl += name; stringstream ss; ss << name << " priority: " << priority << " rest: " << requiresREST; _toString = ss.str(); } { // add to handler list if ( ! _handlers ) _handlers = new vector(); _handlers->push_back( this ); sort( _handlers->begin() , _handlers->end() , prisort ); } } DbWebHandler * DbWebHandler::findHandler( const string& url ) { if ( ! _handlers ) return 0; for ( unsigned i=0; i<_handlers->size(); i++ ) { DbWebHandler * h = (*_handlers)[i]; if ( h->handles( url ) ) return h; } return 0; } vector * DbWebHandler::_handlers = 0; // --- basic handlers --- class FavIconHandler : public DbWebHandler { public: FavIconHandler() : DbWebHandler( "favicon.ico" , 0 , false ) {} virtual void handle( const char *rq, const std::string& url, BSONObj params, string& responseMsg, int& responseCode, vector& headers, const SockAddr &from ) { responseCode = 404; headers.push_back( "Content-Type: text/plain;charset=utf-8" ); responseMsg = "no favicon\n"; } } faviconHandler; class StatusHandler : public DbWebHandler { public: StatusHandler() : DbWebHandler( "_status" , 1 , false ) {} virtual void handle( const char *rq, const std::string& url, BSONObj params, string& responseMsg, int& responseCode, vector& headers, const SockAddr &from ) { headers.push_back( "Content-Type: application/json;charset=utf-8" ); responseCode = 200; static vector commands; if ( commands.size() == 0 ) { commands.push_back( "serverStatus" ); commands.push_back( "buildinfo" ); } BSONObjBuilder buf(1024); for ( unsigned i=0; ilocktype() == 0 ); BSONObj co; { BSONObjBuilder b; b.append( cmd , 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 , 0, errmsg , sub , false ) ) buf.append( cmd , errmsg ); else buf.append( cmd , sub.obj() ); } responseMsg = buf.obj().jsonString(); } } statusHandler; class CommandListHandler : public DbWebHandler { public: CommandListHandler() : DbWebHandler( "_commands" , 1 , true ) {} virtual void handle( const char *rq, const std::string& url, BSONObj params, string& responseMsg, int& responseCode, vector& headers, const SockAddr &from ) { headers.push_back( "Content-Type: text/html;charset=utf-8" ); responseCode = 200; stringstream ss; ss << start("Commands List"); ss << p( a("/", "back", "Home") ); ss << p( "MongoDB List of Commands\n" ); const map *m = Command::commandsByBestName(); ss << "S:slave-ok R:read-lock W:write-lock A:admin-only
\n"; ss << table(); ss << "CommandAttributesHelp\n"; for( map::const_iterator i = m->begin(); i != m->end(); i++ ) i->second->htmlHelp(ss); ss << _table() << _end(); responseMsg = ss.str(); } } commandListHandler; class CommandsHandler : public DbWebHandler { public: CommandsHandler() : DbWebHandler( "DUMMY COMMANDS" , 2 , true ) {} bool _cmd( const string& url , string& cmd , bool& text, bo params ) const { cmd = str::after(url, '/'); text = params["text"].boolean(); return true; } Command * _cmd( const string& cmd ) const { const map *m = Command::webCommands(); if( ! m ) return 0; map::const_iterator i = m->find(cmd); if ( i == m->end() ) return 0; return i->second; } virtual bool handles( const string& url ) const { string cmd; bool text; if ( ! _cmd( url , cmd , text, bo() ) ) return false; return _cmd(cmd) != 0; } virtual void handle( const char *rq, const std::string& url, BSONObj params, string& responseMsg, int& responseCode, vector& headers, const SockAddr &from ) { string cmd; bool text = false; verify( _cmd( url , cmd , text, params ) ); Command * c = _cmd( cmd ); verify( c ); BSONObj cmdObj = BSON( cmd << 1 ); Client& client = cc(); BSONObjBuilder result; Command::execCommand(c, client, 0, "admin.", cmdObj , result, false); responseCode = 200; string j = result.done().jsonString(Strict, text ); responseMsg = j; if( text ) { headers.push_back( "Content-Type: text/plain;charset=utf-8" ); responseMsg += '\n'; } else { headers.push_back( "Content-Type: application/json;charset=utf-8" ); } } } commandsHandler; // --- external ---- void webServerThread(const AdminAccess* adminAccess) { boost::scoped_ptr adminAccessPtr(adminAccess); // adminAccess is owned here Client::initThread("websvr"); const int p = serverGlobalParams.port + 1000; DbWebServer mini(serverGlobalParams.bind_ip, p, adminAccessPtr.get()); mini.setupSockets(); mini.initAndListen(); cc().shutdown(); } } // namespace mongo