/* 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 "stats/snapshots.h"
#include "background.h"
#include "commands.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;
    };

    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';

            ss << bold(ClientCursor::byLocSize()>10000) << "Cursors byLoc.size(): " << ClientCursor::byLocSize() << bold() << '\n';
            ss << "\n<b>replication</b>\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 << "<b>";
            ss <<   "initialSyncCompleted: " << seemCaughtUp;
            if ( !seemCaughtUp ) ss << "</b>";
            ss << '\n';
            
            auto_ptr<SnapshotDelta> delta = statsSnapshots.computeDelta();
            if ( delta.get() ){
                ss << "\n<b>DBTOP  (occurences|percent of elapsed)</b>\n";
                ss << "<table border=1>";
                ss << "<tr align='left'>";
                ss << "<th>NS</th>"
                      "<th colspan=2>total</th>"
                      "<th colspan=2>Reads</th>"
                      "<th colspan=2>Writes</th>"
                      "<th colspan=2>Queries</th>"
                      "<th colspan=2>GetMores</th>"
                      "<th colspan=2>Inserts</th>"
                      "<th colspan=2>Updates</th>"
                      "<th colspan=2>Removes</th>";
                ss << "</tr>";
                
                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 << "</table>";
            }

            statsSnapshots.outputLockInfoHTML( ss );

            BackgroundOperation::dump(ss);
        }

        void display( stringstream& ss , double elapsed , const Top::UsageData& usage ){
            ss << "<td>";
            ss << usage.count;
            ss << "</td><td>";
            double per = 100 * ((double)usage.time)/elapsed;
            ss << setprecision(2) << fixed << per << "%";
            ss << "</td>";
        }

        void display( stringstream& ss , double elapsed , const string& ns , const Top::CollectionData& data ){
            if ( ns != "GLOBAL" && data.total.count == 0 )
                return;
            ss << "<tr><th>" << ns << "</th>";
            
            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 << "</tr>";
        }

        void tablecell( stringstream& ss , bool b ){
            ss << "<td>" << (b ? "<b>X</b>" : "") << "</td>";
        }
        

        template< typename T> 
        void tablecell( stringstream& ss , const T& t ){
            ss << "<td>" << t << "</td>";
        }
        
        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>";
            ss << "<tr align='left'>"
               << "<th>Thread</th>" 
             
               << "<th>OpId</th>" 
               << "<th>Active</th>" 
               << "<th>LockType</th>"
               << "<th>Waiting</th>"
               << "<th>SecsRunning</th>"
               << "<th>Op</th>"
               << "<th>NameSpace</th>"
               << "<th>Query</th>"
               << "<th>client</th>"
               << "<th>msg</th>"
               << "<th>progress</th>"

               << "</tr>\n";
            {
                scoped_lock 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>";
                    
                    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 << "</tr>";
                }
            }
            ss << "</table>\n";
        }
        
        bool allowed( const char * rq , vector<string>& 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<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 ( url.find( "/_status" ) == 0 ){
                    if ( ! allowed( rq , headers, from ) ){
                        responseCode = 401;
                        responseMsg = "not allowed\n";
                        return;
                    }              
                    headers.push_back( "Content-Type: application/json" );
                    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 << "<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);

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

            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 generateServerStatus( string url , string& responseMsg ){
            static vector<string> 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; i<commands.size(); i++ ){
                string cmd = commands[i];

                Command * c = Command::findCommand( cmd );
                assert( c );
                assert( c->locktype() == 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<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 = "";

            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<coll.size(); i++ )
                if ( coll[i] == '/' )
                    coll[i] = '.';

            string fullns = urlDecode(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 , BSONObj & 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"].type() == String && tolower( params["one"].valuestr()[0] ) == 't' ) {
                num = 1;
                one = 1;
            }

            BSONObjBuilder queryBuilder;

            BSONObjIterator i(params);
            while ( i.more() ){
                BSONElement e = i.next();
                string name = e.fieldName();
                if ( ! name.find( "filter_" ) == 0 )
                    continue;

                const char * field = name.substr( 7 ).c_str();
                const char * val = e.valuestr();

                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 );
            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");
        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
