File:  [Isaki's NoNo m68k/m88k emulator] / nono / host / comdriver_tcp.cpp
Revision 1.1.1.4 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:02 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2021 nono project
// Licensed under nono-license.txt
//

//
// シリアルポートの TCP ドライバ
//

#include "comdriver_tcp.h"
#include "config.h"
#include "hostcom.h"
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/telnet.h>

// コンストラクタ
COMDriverTCP::COMDriverTCP(HostDevice *hostdev_)
	: inherited(hostdev_, "tcp")
{
	port = -1;
}

// デストラクタ
COMDriverTCP::~COMDriverTCP()
{
	if (sock.Valid()) {
		hostdev->DelOuter(sock);
		sock.Close();
	}
	if (ls.Valid()) {
		hostdev->DelListen(ls);
		ls.Close();
	}
}

// ドライバ初期化
bool
COMDriverTCP::InitDriver(bool startup)
{
	struct addrinfo hints;
	struct addrinfo *res, *ai;
	int r;

	// host, port を取得
	const char *host = "::1";
	std::string key = hostdev->GetConfigKey();
	const ConfigItem& item = gConfig->Find(key + "-tcp-port");
	std::string portstr = item.AsString();
	port = atoi(portstr.c_str());
	if (port < 1 || port > 65535) {
		port = -1;
		errmsg = item.ErrMsg();
		putmsg(1, "%s", errmsg.c_str());
		return false;
	}

	// hints を用意
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE;

	r = getaddrinfo(host, portstr.c_str(), &hints, &res);
	if (r != 0) {
		const char *em;
		if (r == EAI_SYSTEM) {
			em = strerror(errno);
		} else {
			em = gai_strerror(r);
		}
		errmsg = item.ErrMsg("getaddrinfo(%s): %s", host, em);
		putmsg(1, "%s", errmsg.c_str());
		return false;
	}
	if (res == NULL) {
		errmsg = item.ErrMsg("getaddrinfo(%s): no address found", host);
		putmsg(1, "%s", errmsg.c_str());
		return false;
	}

	// res を順番に試して成功したらそれで待ち受ける
	ls = -1;
	for (ai = res; ai && ls == -1; ai = ai->ai_next) {
		int on = 1;

		ls = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
		if (ls == -1) {
			errmsg = string_format("socket(family=%d): %s",
				(int)ai->ai_family, strerror(errno));
			putmsg(1, "%s", errmsg.c_str());
			continue;
		}

		if (setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) {
			errmsg = string_format("setsockopt(SO_REUSEADDR): %s",
				strerror(errno));
			putmsg(1, "%s", errmsg.c_str());
			ls.Close();
			continue;
		}

		if (setsockopt(ls, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on))) {
			errmsg = string_format("setsockopt(SO_REUSEPORT): %s",
				strerror(errno));
			putmsg(1, "%s", errmsg.c_str());
			ls.Close();
			continue;
		}

		if (bind(ls, ai->ai_addr, ai->ai_addrlen) == -1) {
			errmsg = string_format("bind: %s", strerror(errno));
			putmsg(1, "%s", errmsg.c_str());
			ls.Close();
			continue;
		}

		if (listen(ls, 0) == -1) {
			errmsg = string_format("listen: %s", strerror(errno));
			putmsg(1, "%s", errmsg.c_str());
			ls.Close();
			continue;
		}

		// ここまで来たら成功
		break;
	}

	freeaddrinfo(res);

	if (ls == -1) {
		return false;
	}
	if (hostdev->AddListen(ls, EV_ADD) < 0) {
		errmsg = string_format("AddListen(ADD): %s", strerror(errno));
		putmsg(1, "%s", errmsg.c_str());
		return false;
	}

	return true;
}

// Driver の Dispatch() は udata を返す。
int
COMDriverTCP::Dispatch(int udata)
{
	if (udata == HostDevice::LISTEN_SOCKET) {
		if (Accept()) {
			// 呼び出し側に戻って通知を出したいので LISTEN_SOCKET のまま帰る
			return udata;
		} else {
			// 失敗時は何もしない
			return HostDevice::DONE;
		}
	}

	return udata;
}

// 待ち受けソケットに着信した。
// 成功すれば true を返す。
bool
COMDriverTCP::Accept()
{
	struct sockaddr addr;
	socklen_t addrlen;

	for (;;) {
		addrlen = sizeof(addr);
		sock = accept(ls, &addr, &addrlen);
		if (sock < 0) {
			if (errno == EINTR) {
				continue;
			}
			// XXX どうする?
			putmsg(0, "accept: %s", strerror(errno));
			return false;
		}
		break;
	}

	// 待ち受けソケットのほうは一旦通知オフにする?
	if (hostdev->AddListen(ls, EV_DISABLE) < 0) {
		putmsg(0, "AddListen(DISABLE): %s", strerror(errno));
		return false;
	}

	putmsg(1, "Accept a connection");

	// エコーオフ
	WriteCmd({ WILL, TELOPT_ECHO });
	negotiation++;

	// 全二重を使用
	WriteCmd({ WILL, TELOPT_SGA });
	negotiation++;

	// コマンド送信中にエラーが起きたら終了
	if (sock.Valid() == false) {
		return false;
	}

	// 新しいソケットを kevent に登録
	if (hostdev->AddOuter(sock) < 0) {
		putmsg(0, "AddOuter: %s", strerror(errno));
		Close();
		return false;
	}

	return true;
}

// 外部への1バイト送信
void
COMDriverTCP::Write(uint32 data)
{
	uint8 buf[1];
	int n;

	if (sock.Valid()) {
		buf[0] = data;

		n = write(sock, buf, sizeof(buf));
		if (n < 0) {
			putmsg(0, "write: %s", strerror(errno));
			// エラーならソケットをクローズ
			Close();
			return;
		}
		if (n < sizeof(buf)) {
			putmsg(0, "write: short");
			return;
		}
	}
}

// TELNET コマンドを送信。
// cmd には IAC を除いたデータ列を指定する。
void
COMDriverTCP::WriteCmd(const std::vector<uint8>& cmd)
{
	if (loglevel >= 1) {
		std::string str;
		for (auto data : cmd) {
			str += ' ';
			str += GetCommandName(data);
		}
		putmsgn("Send %s", str.c_str() + 1);
	}

	Write(IAC);
	for (auto data : cmd) {
		Write(data);
	}
}

// 外部からの1バイト受信
int
COMDriverTCP::Read()
{
	uint8 buf[1];
	int r;

	r = read(sock, buf, sizeof(buf));
	if (r < 0) {
		putmsg(0, "read: %s", strerror(errno));
		Close();
		return NODATA;
	}
	if (r == 0) {
		Close();
		return NODATA;
	}
	uint8 data = buf[0];

	if (__predict_false(recvcmd.empty() == false || data == IAC)) {
		// TELNET コマンド
		return Command(data);
	} else {
		// 平文
		return data;
	}
}

// データソケットをクローズして、待ち受けソケットの通知を再開する。
void
COMDriverTCP::Close()
{
	putmsg(1, "Close the connection");

	// データソケットをクローズ
	if (sock.Valid()) {
		hostdev->DelOuter(sock);
		sock.Close();
	}

	// 待ち受けソケットを再開
	if (hostdev->AddListen(ls, EV_ENABLE) < 0) {
		putmsg(0, "AddListen(ENABLE): %s", strerror(errno));
		return;
	}
}

// TELNET コマンド受信。
// VM 側に文字を渡す場合は 0..255 を返す。
// VM に文字を渡さない場合は NODATA を返す。
int
COMDriverTCP::Command(uint8 code)
{
	recvcmd.push_back(code);
	putmsg(2, "Read cmd $%02x %s", code, GetCommandName(code).c_str());

	if (cmdstate == WILL) {
		// WILL: 送信側がこのオプション使用を宣言
		// DONT: 送信側がこのオプション無効を宣言
		// (今の所動作が同じなので同じ state を使う)

		if (negotiation > 0) {
			// 送ったコマンドに対する応答を受信したのなら、何もしない
			putmsg(1, "Recv response %s %s",
				GetCommandName(recvcmd[1]).c_str(),
				GetCommandName(recvcmd[2]).c_str());
			negotiation--;
		} else {
			// 向こう発のコマンドを受信したのなら、応答する
			putmsg(1, "Recv command %s %s",
				GetCommandName(recvcmd[1]).c_str(),
				GetCommandName(recvcmd[2]).c_str());

			// どのコマンドもサポートしていないので、
			// WILL に対しては DONT で無視。
			// DONT に対しては DONT で応答。
			WriteCmd({ DONT, recvcmd[2] });
		}

		recvcmd.clear();
		cmdstate = 0;

	} else if (cmdstate == DO) {
		// DO:   送信側がこちらにこのオプション使用を要求
		// WONT: 送信側がこちらにこのオプション無効を要求
		// (今の所動作が同じなので同じ state を使う)

		if (negotiation > 0) {
			// 送ったコマンドに対する応答を受信したのなら、何もしない
			putmsg(1, "Recv response %s %s",
				GetCommandName(recvcmd[1]).c_str(),
				GetCommandName(recvcmd[2]).c_str());
			negotiation--;
		} else {
			// 向こう発のコマンドを受信したのなら、応答する
			putmsg(1, "Recv command %s %s",
				GetCommandName(recvcmd[1]).c_str(),
				GetCommandName(recvcmd[2]).c_str());

			// DO   に対しては WONT で拒否。
			// WONT に対しては WONT で応答。
			WriteCmd({ WONT, recvcmd[2] });
		}

		recvcmd.clear();
		cmdstate = 0;

	} else if (cmdstate == SB) {
		// IAC, SE で終端
		if (recvcmd[recvcmd.size() - 2] == IAC &&
		    recvcmd[recvcmd.size() - 1] == SE    )
		{
			// 未知のシーケンスを読み飛ばすだけで、何もしない
			recvcmd.clear();
			cmdstate = 0;
		}

	} else if (recvcmd.size() < 2) {
		// 1バイト目は IAC のはず

	} else if (recvcmd.size() == 2) {
		// 2バイト目がコマンド
		cmdstate = 0;
		switch (recvcmd[1]) {
		 case SE:	// SE はここに来ないはずだが、来てもここで終了でいい。
		 case NOP:	// NOP はたぶん何もしない。
			FALLTHROUGH;
		 case DM:
			// XXX 未確認
			FALLTHROUGH;
		 case BREAK:
			// XXX Break 文字を送る
			FALLTHROUGH;
		 case IP:
		 case AO:
		 case AYT:
		 case EC:
		 case EL:
		 case GA:
			// XXX 未実装、たぶん引数はない
			putmsg(1, "Recv command %s", GetCommandName(recvcmd[1]).c_str());
			recvcmd.clear();
			break;

		 case SB:
			// この後 SE までがサブネゴシエーション
			putmsg(1, "Recv command %s", GetCommandName(recvcmd[1]).c_str());
			cmdstate = SB;
			break;

		 case WILL:
		 case DONT:
			// 次の1文字がオプション
			cmdstate = WILL;
			break;
		 case DO:
		 case WONT:
			// 次の1文字がオプション
			cmdstate = DO;
			break;

		 default:
			// それ以外なら、その文字
			recvcmd.clear();
			return code;
		}
	}

	return NODATA;
}

// TELNET のコマンド、オプション名を返す
/*static*/ std::string
COMDriverTCP::GetCommandName(uint8 code)
{
	switch (code) {
	 case 0x00:	return "Transmit-Binary";
	 case 0x01:	return "Echo";
	 case 0x03:	return "Suppress-Go-Ahead";
	 case 0x05:	return "Telnet-Status-Option";
	 case 0x06:	return "Telnet-Timing-Mark";
	 case 0x18:	return "Terminal-Type";
	 case 0x22:	return "Telnet-Line-Mode";

	 case 0xf0:	return "SE";
	 case 0xf1:	return "NOP";
	 case 0xf2:	return "DataMark";
	 case 0xf3:	return "Break";
	 case 0xf4:	return "IP";
	 case 0xf5:	return "AO";	// Abort Output
	 case 0xf6:	return "AYT";	// Are You There
	 case 0xf7:	return "EC";	// Erase Character
	 case 0xf8:	return "EL";	// Erase Line
	 case 0xf9:	return "GA";	// Go Ahead
	 case 0xfa:	return "SB";
	 case 0xfb:	return "WILL";
	 case 0xfc:	return "WONT";
	 case 0xfd:	return "DO";
	 case 0xfe:	return "DONT";
	 case 0xff:	return "IAC";	// Interpret As Command

	 default:
		return string_format("(unknown $%02x)", code);
	}
}

// モニタ (ドライバ依存情報のみ)
void
COMDriverTCP::MonitorScreenMD(TextScreen& screen, int y)
{
	screen.Print(0, y++, "Listen Port: %u", port);
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.