OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-cogserver / opencog / cogserver / server / ServerConsole.cc

442 lines · cc ref:
/*
 * opencog/cogserver/server/ServerConsole.cc
 *
 * Copyright (C) 2002-2007 Novamente LLC
 * Written by Andre Senna <senna@vettalabs.com>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

#include <time.h>

#include <string>

#include <opencog/util/Logger.h>
#include <opencog/util/misc.h>
#include <opencog/util/oc_assert.h>

#include <opencog/atoms/atom_types/atom_names.h>
#include <opencog/atoms/value/BoolValue.h>
#include <opencog/atoms/value/StringValue.h>

#include <opencog/cogserver/server/CogServer.h>
#include <opencog/cogserver/server/ServerConsole.h>
#include <opencog/cogserver/server/Request.h>

using namespace opencog;

ServerConsole::ServerConsole(const Handle& hcsn, CogServer& cs, SocketManager* mgr) :
	ConsoleSocket(mgr),
	_cserver(cs)
{
	// Get AtomSpace from CogServerNode
	AtomSpace* asp = hcsn->getAtomSpace();

	// Check if ANSI colors are enabled
	bool ansi_enabled = true;
	ValuePtr vp = hcsn->getValue(asp->add_atom(Predicate("*-ansi-enabled-*")));

	// If *-ansi-enabled-* is mising entirely, assume this is a hush
	// connection, and sent no prompt at all. This save a bit of time
	// decoding teh assorted prompt strings..
	if (nullptr == vp or not vp->is_type(BOOL_VALUE)) return;

	// Get the appropriate prompt
	ansi_enabled = BoolValueCast(vp)->value()[0];
	if (ansi_enabled)
		vp = hcsn->getValue(asp->add_atom(Predicate("*-ansi-prompt-*")));
	else
		vp = hcsn->getValue(asp->add_atom(Predicate("*-prompt-*")));

	if (vp and vp->is_type(STRING_VALUE))
		_prompt = StringValueCast(vp)->value()[0];
	else if (vp and vp->is_node())
		_prompt = HandleCast(vp)->get_name();
}

ServerConsole::~ServerConsole()
{
}

// Some random RFC 854 characters
#define IAC  0xff // Telnet Interpret As Command
#define TEOF 0xec // Telnet EOF      RFC 1184 linemode EOF   236
#define SUSP 0xed // Telnet suspend  RFC 1184 linemode SUSP  237
#define ABRT 0xee // Telnet abort    RFC 1184 linemode ABORT 238
#define SE   0xf0 // Telnet SE End of subnegotiation
#define NOP  0xf1 // Telnet NOP no-op
#define MARK 0xf2 // Telnet Data Mark
#define BRK  0xf3 // Telnet break
#define IP   0xf4 // Telnet IP Interrupt Process
#define AO   0xf5 // Telnet AO Abort Output
#define AYT  0xf6 // Telnet AYT Are You There
#define EC   0xf7 // Telnet EC Erase Character
#define EL   0xf8 // Telnet EL Erase Line
#define GA   0xf9 // Telnet GA Go ahead
#define SB   0xfa // Telnet SB Begin subnegotiation
#define WILL 0xfb // Telnet WILL
#define WONT 0xfc // Telnet WONT
#define DO   0xfd // Telnet DO
#define DONT 0xfe // Telnet DONT

#define TRANSMIT_BINARY   0  // Telnet RFC 856 8-bit-clean binary transmission
#define RFC_ECHO          1  // Telnet RFC 857 ECHO option
#define SUPPRESS_GO_AHEAD 3  // Telnet RFC 858 supporess go ahead
#define TIMING_MARK       6  // Telnet RFC 860 timing mark
#define WINSIZE          31  // Telnet RFC 1073 window size
#define SPEED            32  // Telnet RFC 1079 terminal speed
#define LINEMODE         34  // Telnet RFC 1116 linemode
#define ENVIRON          39  // Telnet RFC 1572 environment variables
#define CHARSET          42  // Telnet RFC 2066

/// Return true if the Telnet IAC command was rcognized and handled.
// The code here is similar to that in GenericShell, but different.
// The Console is not a full-fledged shell, but a mini-shell-like
// thing that just handles Requests. It is much simpler than the
// GenericShell, which is why we keep it around here: its a more
// fail-safe, less complex thing that is easier to maintain. The
// downside is that both have similar telnet line discipline.
bool ServerConsole::handle_telnet_iac(const std::string& line)
{
	// Hmm. Most telnet agents respond with an
	// IAC WONT CHARSET IAC DONT CHARSET
	// Ignore CHARSET RFC 2066 negotiation

	int sz = line.size();
	int i = 0;
	while (i < sz)
	{
		unsigned char iac = line[i];
		if (IAC != iac) return false;
		i++; if (sz <= i) return false;
		unsigned char c = line[i];
		i++;

		logger().debug("[ServerConsole] %d Received telnet IAC %d", i, c);

		// Actually, ignore all WONT & DONT.
		if (WONT == c or DONT == c)
		{
			// Skip over the argument and advance
			if (sz <= i) return false;
			i++;
			continue;
		}

		if (DO == c)
		{
			if (sz <= i) return false;
			unsigned char a = line[i];
			i++;

			logger().debug("[ServerConsole] Received IAC DO %d", a);

			// If telnet ever tries to go into character mode,
			// it will send us SUPPRESS-GO-AHEAD and ECHO. Try to
			// stop that; we don't want to effing fiddle with that.
			// The putty telnet client tries to do this, and it won't
			// work unless we say WONT.
			if (SUPPRESS_GO_AHEAD == a)
			{
				unsigned char ok[] = {IAC, WONT, SUPPRESS_GO_AHEAD, 0};
				Send((const char *) ok);
				continue;
			}
			if (RFC_ECHO == a)
			{
				unsigned char ok[] = {IAC, WONT, RFC_ECHO, 0};
				Send((const char *) ok);
				continue;
			}
			if (TRANSMIT_BINARY == a)
			{
				logger().debug("[ServerConsole] Sending IAC WILL TRANSMIT_BINARY");
				unsigned char ok[] = {IAC, WILL, TRANSMIT_BINARY, 0};
				Send((const char *) ok);
				continue;
			}

			// Just say we won't do anything more.
			// PuTTY will give us 31, 32, 34, 39
			// which is WINSIZE SPEED LINEMODE ENVIRON
			logger().debug("[ServerConsole] Sending IAC WONT %d", a);
			unsigned char ok[] = {IAC, WONT, a, 0};
			Send((const char *) ok);
			continue;
		}

		if (WILL == c)
		{
			if (sz <= i) return false;
			unsigned char a = line[i];
			i++;

			// Refuse to perform sub-negotation when
			// IAC WILL LINEMODE is sent by the telnet client.
			if (LINEMODE == a)
			{
				unsigned char ok[] = {IAC, DONT, LINEMODE, 0};
				Send((const char *) ok);
				continue;
			}

			// Ignore anything else.
			logger().debug("[ServerConsole] Ignoring telnet IAC WILL %d", a);
			continue;
		}

		if (IP == c or AO == c or SUSP == c)
		{
			logger().debug("[ServerConsole] Got telnet IAC user-interrupt %d", c);
			// Telnet keeps resending us the stuff before the ctrl-C.
			// I want it to throw that away, dut it doesn't.
			// Whatever. I give up.
			// unsigned char ok[] = {IAC, EL, 0};
			// unsigned char ok[] = {IAC, DO, TIMING_MARK, 0};
			unsigned char ok[] = {'\n', 0};
			Send((const char *) ok);
			continue;
		}

		// Erase Line -- just ignore this line.
		if (EL == c or EC == c or ABRT == c or
		    AYT == c or GA == c or NOP == c)
		{
			logger().debug("[ServerConsole] Ignore line; got telnet IAC %d", c);
			continue;
		}

		// Break -- just ignore this line.
		if (BRK == c)
		{
			logger().debug("[ServerConsole] Received IAC BRK");
			continue;
		}

		// End-of-file just like ctrl-D
		if (TEOF == c)
		{
			logger().debug("[ServerConsole] Received IAC EOF; exiting shell");
			return false;
		}
	}

	return true;
}

void ServerConsole::OnConnection()
{
    logger().debug("[ServerConsole] OnConnection");

#ifdef NOT_RIGHT_NOW
    // Crude attempt to negotiate for a utf-8 clean channel.
    // Using RFC 2066 protocols.  Not robust.  We're just praying
    // for non-garbled UTF-8 goodness, here.
    //
    // As of 2021 Debian Buster stable, telnet is not 8-bit clean.
    // It also responds DONT/WONT to the CHARSET negotiatin below.
    // So, as of now, we can't have UTF-8 with Debian telnet. Bummer.
    //
    // Anyway, this won't work for netcat, socat, because they'll
    // just pass all this crap back to the user, and we don't want
    // that.  I'm not sure how to tell if we're talking to a true
    // RFC telnet. Maybe we need to listen to a different port?
    char utf_plz[20];
    utf_plz[0] = IAC;
    utf_plz[1] = WILL;
    utf_plz[2] = CHARSET;
    utf_plz[3] = 0;
    Send(utf_plz);
    utf_plz[1] = DO;
    Send(utf_plz);

    utf_plz[0] = IAC;
    utf_plz[1] = SB;
    utf_plz[2] = CHARSET;
    utf_plz[3] = '\02';
    utf_plz[4] = 'U';
    utf_plz[5] = 'T';
    utf_plz[6] = 'F';
    utf_plz[7] = '-';
    utf_plz[8] = '8';
    utf_plz[9] = IAC;
    utf_plz[10] = SE;
    utf_plz[11] = 0;
    Send(utf_plz);
#endif

    sendPrompt();
}

void ServerConsole::sendPrompt()
{
    // Hush prompts are empty. Don't call.
    if (0 < _prompt.size())
        Send(_prompt);
}


/// Parse command line. Quotes are stripped.
/// XXX escaped quotes are not handled correctly. FIXME.
/// This passes over quotes embedded in the middle strings.
/// And that OK, because what the heck did you want to happen?
static std::list<std::string> simple_tokenize(const std::string& line)
{
    std::list<std::string> params;
    size_t pos = 0;
    size_t len = line.size();
    while (pos<len) {
        // Gather up everything until the next quote.
        if ('\"' == line[pos]) {
            std::string buff;
            pos++; // skip over opening quote
            while ('\"' != line[pos] and pos < len) {
               buff.push_back(line[pos]);
               pos++;
            }
            pos++; // skip over closing quote
            params.push_back(buff);
            if (len <= pos) break;
            continue;
        }

        // Gather up everything until the next blank space.
        if (' ' != line[pos]) {
            std::string buff;
            while (' ' != line[pos] and pos < len) {
               buff.push_back(line[pos]);
               pos++;
            }
            params.push_back(buff);
            if (len <= pos) break;
            continue;
        }

        // Skip over blank spaces.
        if (' ' == line[pos]) {
            while (' ' == line[pos] and pos < len) pos++;
            if (len <= pos) break;
            continue;
        }
    }
    return params;
}

void ServerConsole::OnLine(const std::string& line)
{
    // If a shell processor has been designated, then defer all
    // processing to the shell.  In particular, avoid as much overhead
    // as possible, since the shell needs to be able to handle a
    // high-speed data feed with as little getting in the way as
    // possible.
    if (_shell) {
        _shell->eval(line);
        return;
    }

    // If the cogserver has stopped, then the command processor is gone,
    // and we just ... can't handle commands any longer. Self-destruct.
    CogServer& cs = _cserver;
    if (not cs.running())
    {
        Exit();
        return;
    }

    // Look for telnet stuff, and process it.
    if (IAC == (line[0] & 0xff)
        and line.size() < 40
        and handle_telnet_iac(line))
    {
        sendPrompt();
        return;
    }

    // If the command starts with an open-paren, or a semi-colon, assume
    // its a scheme command. Pop into the scheme shell, and try again.
    if (line[0] == '(' or line[0] == ';')
    {
        OnLine("scm");

        // Re-issue the command, but only if we successfully got a shell.
        // (We might not get a shell if scheme is not installed.)
        if (_shell) {
            OnLine(line);
            return;
        }
    }

    // If the command starts with an open-brace, assume
    // its a json command. Pop into the json shell, and try again.
    else if (line[0] == '{')
    {
        OnLine("json");

        // Re-issue the command, but only if we successfully got a shell.
        // (We might not get a shell if json is screwed up.)
        if (_shell) {
            OnLine(line);
            return;
        }
    }

    logger().debug("[ServerConsole] OnLine [%s]", line.c_str());

    // Parse command line. Quotes are stripped.
    // tokenize(line, std::back_inserter(params), " \t\v\f");
    std::list<std::string> params = simple_tokenize(line);

    logger().debug("params.size(): %d", params.size());
    if (params.empty()) {
        // return on empty/blank line
        sendPrompt();
        return;
    }

    std::string cmdName = params.front();
    params.pop_front();

    Request* request = cs.createRequest(cmdName);

    // Command not found.
    if (nullptr == request)
    {
        char msg[256];
        snprintf(msg, 256, "command \"%s\" not found\n", cmdName.c_str());
        logger().debug("%s", msg);
        Send(msg);

        // Try to send "help" command response
        request = cs.createRequest("help");
        if (nullptr == request)
        {
            // no help request; just terminate the request
            sendPrompt();
            return;
        }
    }

    request->set_console(this);
    request->setParameters(params);
    bool is_shell = request->isShell();

    // Add the command to the processing queue.
    // Caution: after the pushRequest, the request might be executed
    // and then deleted in a different thread. It must NOT be accessed
    // after the push!
    cs.pushRequest(request);

    if (is_shell)
    {
        logger().debug("ServerConsole::OnLine() request \"%s\" is a shell",
                       line.c_str());

        // Force a drain of the request queue, because we *must* enter
        // shell mode before handling any additional input from the
        // socket (since all subsequent input will be for the new shell,
        // not for the cogserver command processor).
        cs.processRequests();
    }
}

// ==================================================================