/* 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/>.
*
*    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 <boost/date_time/posix_time/posix_time.hpp>
#include <pcrecpp.h>

#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/ramlog.h"
#include "mongo/util/version.h"
#include "mongo/util/version_reporting.h"


namespace mongo {

    using namespace mongoutils::html;
    using namespace bson;

namespace {

    void doUnlockedStuff(stringstream& ss) {
        /* this is in the header already ss << "port:      " << port << '\n'; */
        ss << "<pre>";
        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 << "</pre>";
    }


    bool prisort(const Prioritizable * a, const Prioritizable * b) {
        return a->priority() < b->priority();
    }


    struct Timing {
        Timing() {
            start = timeLocked = 0;
        }
        unsigned long long start, timeLocked;
    };


    class LogPlugin : public WebStatusPlugin {
    public:
        LogPlugin()
            : WebStatusPlugin("Log", 100),
              _log(RamLog::get("global")) {

        }

        virtual void init() {}

        virtual void run(stringstream& ss) {
            _log->toHTML(ss);
        }

    private:
        RamLog* const _log;
    };


    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<string>& 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<string>& headers,  const SockAddr &from ) {
            headers.push_back( "Content-Type: application/json;charset=utf-8" );
            responseCode = 200;

            static vector<string> commands;
            if ( commands.size() == 0 ) {
                commands.push_back( "serverStatus" );
                commands.push_back( "buildinfo" );
            }

            BSONObjBuilder buf(1024);

            for ( unsigned i=0; i<commands.size(); i++ ) {
                string cmd = commands[i];

                Command * c = Command::findCommand( cmd );
                verify( c );
                verify( c->locktype() == 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<string>& 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( "<b>MongoDB List of <a href=\"http://dochub.mongodb.org/core/commands\">Commands</a></b>\n" );
            const map<string, Command*> *m = Command::commandsByBestName();
            ss << "S:slave-ok  R:read-lock  W:write-lock  A:admin-only<br>\n";
            ss << table();
            ss << "<tr><th>Command</th><th>Attributes</th><th>Help</th></tr>\n";
            for( map<string, Command*>::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<string,Command*> *m = Command::webCommands();
            if( ! m )
                return 0;

            map<string,Command*>::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<string>& 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;


    MONGO_INITIALIZER(WebStatusLogPlugin)(InitializerContext*) {
        if (serverGlobalParams.isHttpInterfaceEnabled) {
            new LogPlugin();
        }
        return Status::OK();
    }

} // namespace


    DbWebServer::DbWebServer(const string& ip, int port, const AdminAccess* webUsers)
            : MiniWebServer("admin web console", ip, port),
              _webUsers(webUsers) {

        WebStatusPlugin::initAll();
    }

    bool DbWebServer::_allowed(const char * rq, vector<string>& 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<string,string> 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"] ) {
                    Status status = cc().getAuthorizationSession()->addAndAuthorizeUser(userName);
                    uassertStatusOK(status);

                    return true;
                }
            }
        }

        stringstream authHeader;
        authHeader
                << "WWW-Authenticate: "
                << "Digest realm=\"mongo\", "
                << "nonce=\"abc\", "
                << "algorithm=MD5, qop=\"auth\" "
                ;

        headers.push_back( authHeader.str() );
        return 0;
    }

    void DbWebServer::doRequest(const char *rq,
                                string url,
                                string& responseMsg,
                                int& responseCode,
                                vector<string>& headers,
                                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 = "<html><body>unknown url</body></html>\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 << "<p><a href=\"/_commands\">List all commands</a> | \n";
        ss << "<a href=\"/_replSet\">Replica set status</a></p>\n";

        //ss << "<a href=\"/_status\">_status</a>";
        {
            const map<string, Command*> *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<string, Command*>::const_iterator i = m->begin(); i != m->end(); i++ ) {
                    stringstream h;
                    i->second->help(h);
                    string help = h.str();
                    ss << "<a href=\"/" << i->first << "?text=1\"";
                    if( help != "no help defined" )
                        ss << " title=\"" << help << '"';
                    ss << ">" << i->first << "</a> ";
                }
                ss << '\n';
            }
        }
        ss << '\n';

        doUnlockedStuff(ss);

        WebStatusPlugin::runAll( ss );

        ss << "</body></html>\n";
        responseMsg = ss.str();
        headers.push_back( "Content-Type: text/html;charset=utf-8" );
    }

    void DbWebServer::_rejectREST(string& responseMsg,
                                  int& responseCode,
                                  vector<string>& 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");
    }
    

    WebStatusPlugin::WebStatusPlugin( const string& secionName , double priority , const string& subheader )
        : Prioritizable(priority), _name( secionName ) , _subHeading( subheader ) {
        if ( ! _plugins )
            _plugins = new vector<WebStatusPlugin*>();
        _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 << "<hr>\n"
               << "<b>" << p->_name << "</b>";

            ss << " " << p->_subHeading;

            ss << "<br>\n";

            p->run(ss);
        }

    }

    vector<WebStatusPlugin*> * WebStatusPlugin::_plugins = 0;

    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<DbWebHandler*>();
            _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*> * DbWebHandler::_handlers = 0;


    void webServerListenThread(boost::shared_ptr<DbWebServer> dbWebServer) {
        Client::initThread("websvr");

        dbWebServer->initAndListen();

        cc().shutdown();
    }

} // namespace mongo
