hostile-takeover/game/SocTransport.cpp
2016-08-31 23:55:30 -04:00

820 lines
20 KiB
C++

#ifdef PNO
#define USE_PALM_UNIX_HEADERS
#endif
#include "ht.h"
#include "mpshared/netmessage.h"
#include "SocTransport.h"
namespace wi {
#ifdef DEBUG
const TCHAR *PszFromSocError() secComm;
const TCHAR *PszFromSocError(int nError) secComm;
#endif
//---------------------------------------------------------------------------
// SocTransport implementation
SocTransport::SocTransport(dword dwIpAddress)
{
m_socBroadcast = INVALID_SOCKET;
m_socBroadcastListen = INVALID_SOCKET;
m_socAcceptListen = INVALID_SOCKET;
m_dwIpAddress = dwIpAddress;
m_pconAsyncConnect = NULL;
}
bool SocTransport::GetLocalNetAddress(NetAddress *pnad)
{
memset(pnad, 0, sizeof(*pnad));
sockaddr_in *psoca = (sockaddr_in *)pnad;
psoca->sin_family = AF_INET;
psoca->sin_port = 0;
#ifdef IPHONE
psoca->sin_addr.s_addr = htonl(m_dwIpAddress);
#else
psoca->sin_addr.S_un.S_addr = htonl(m_dwIpAddress);
#endif
return true;
}
// Stub so we can force the code section this destructor goes in
SocTransport::~SocTransport()
{
}
void SocTransport::GetAddressString(char *pszAddress, int cbMax)
{
in_addr ina;
#ifdef IPHONE
ina.s_addr = htonl(m_dwIpAddress);
#else
ina.S_un.S_addr = htonl(m_dwIpAddress);
#endif
strncpyz(pszAddress, inet_ntoa(ina), cbMax);
}
// UNDONE: this blocks in connect() (not Async!)
Connection *SocTransport::AsyncConnect(NetAddress *pnad)
{
MpTrace("AsyncConnect(%s)", pnad != NULL ? inet_ntoa(((sockaddr_in *)pnad)->sin_addr) : "loopback");
Connection *pcon;
// A NULL pnad means this device is the server and is now trying to
// connect to itself as a client. Use the LoopbackConnection to satisfy
// this.
if (pnad == NULL) {
pcon = new LoopbackConnection(this);
Assert(pcon != NULL, "out of memory!");
if (pcon == NULL)
return NULL;
m_fAsyncConnectLoopback = true;
} else {
// Create the socket for connecting to the server
SOCKET soc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (soc == INVALID_SOCKET) {
#ifdef DEBUG
HostMessageBox(TEXT("socket err: 0x%lx"), PszFromSocError());
#endif
return NULL;
}
// Disable nagle algorithm
int nTrue = 1;
setsockopt(soc, IPPROTO_TCP, TCP_NODELAY, (NetBuff)&nTrue, sizeof(int));
// Disable excessive 'lingering' (particularly a problem on PalmOS <=5 which has only 16 sockets)
linger lngr;
lngr.l_onoff = 1;
lngr.l_linger = 0;
setsockopt(soc, SOL_SOCKET, SO_LINGER, (NetBuff)&lngr, sizeof(lngr));
// Connect to the server
// UNDONE: On PalmOS <=5 this occasionally times out after 2 secs. Longer would be better
if (connect(soc, (const sockaddr *)pnad, sizeof(sockaddr_in)) != 0) {
switch (WSAGetLastError()) {
case WSAEHOSTUNREACH:
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Host unreachable.");
break;
case WSAECONNREFUSED:
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Connection refused.");
break;
case WSAETIMEDOUT:
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Timed out trying to connect to host.");
break;
default:
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Unable to connect. Error %d", WSAGetLastError());
break;
}
closesocket(soc);
return NULL;
}
pcon = NewConnection(soc);
if (pcon == NULL) {
closesocket(soc);
return NULL;
}
m_fAsyncConnectLoopback = false;
}
AddConnection(pcon);
Assert(m_pconAsyncConnect == NULL, "Can only have one pending AsyncConnect at a time");
m_pconAsyncConnect = pcon;
return pcon;
}
SocConnection *SocTransport::NewConnection(SOCKET soc)
{
return new SocConnection(soc);
}
void SocTransport::Poll()
{
if (!IsOpen())
return;
// Users of Transport::AsyncConnect rely on it not completing the connection
// immediately so they have an opportunity to hook the OnConnectComplete
// callback.
if (m_pconAsyncConnect != NULL) {
if (m_fAsyncConnectLoopback)
((LoopbackConnection *)m_pconAsyncConnect)->Connect();
IConnectionCallback *pccb = m_pconAsyncConnect->GetCallback();
if (pccb != NULL) {
pccb->OnConnectComplete(m_pconAsyncConnect);
}
m_pconAsyncConnect = NULL;
}
// While a game is being advertised the server accepts client connections
if (m_socAcceptListen != INVALID_SOCKET) {
// Test if any client connection requests are pending
fd_set fds;
FD_ZERO(&fds);
FD_SET(m_socAcceptListen, &fds);
TIMEVAL tvTimeout;
tvTimeout.tv_sec = 0;
tvTimeout.tv_usec = 0;
int nSelected = select(((int)m_socAcceptListen) + 1, &fds, NULL, NULL, &tvTimeout);
if (nSelected == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("select err: %s"), PszFromSocError());
#endif
if (m_ptcb != NULL)
m_ptcb->OnTransportError(ktraeAdvertiseGameFailed);
return;
}
// Accept a client connection and produce a new socket for communicating with that client
if (nSelected == 1) {
NetAddress nad;
memset(&nad, 0, sizeof(nad));
#ifdef IPHONE
socklen_t cbAddr = sizeof(nad);
#else
int cbAddr = sizeof(nad);
#endif
SOCKET socConn = accept(m_socAcceptListen, (sockaddr *)&nad, &cbAddr);
if (socConn == INVALID_SOCKET) {
// This error is seen on a Tungsten C hosting a game when a ux50
// times out while trying to connect. The best thing to do is just
// ignore the connection attempt. Hopefully it will try again and
// get it right this time
if (WSAGetLastError() == 0)
return;
#ifdef DEBUG
HostMessageBox(TEXT("accept err: %s"), PszFromSocError());
#endif
return;
}
// HostMessageBox("cbAddr = %d, sizeof(sockaddr_in) = %d", cbAddr, sizeof(sockaddr_in));
// Assert(cbAddr == sizeof(sockaddr_in));
Connection *pcon = new SocConnection(socConn);
Assert(pcon != NULL, "out of memory!");
if (pcon == NULL) {
closesocket(socConn);
return;
}
AddConnection(pcon);
if (m_ptcb != NULL) {
if (!m_ptcb->OnClientConnect(pcon))
delete pcon;
}
}
}
Transport::Poll();
}
// BeginGameSearch is called when the JoinOrHostMultiplayer form enters its
// modal loop. EndGameSearch is called when the form is destructed.
// Winsock GameSearch
// - create a non-blocking datagram socket to listen for SERVERINFO broadcasts
// - poll the socket to check for SERVERINFO broadcasts every 1/10th of a second
// - for each SERVERINFO broadcast received call the Transport's registered
// ITransportCallback::OnReceive method
bool SocTransport::BeginGameSearch()
{
// Already searching?
if (m_socBroadcastListen != INVALID_SOCKET)
return false;
m_fBlockNextGameHost = false;
m_socBroadcastListen = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_socBroadcastListen == INVALID_SOCKET) {
#ifdef DEBUG
HostMessageBox(TEXT("socket err: %s"), PszFromSocError());
#endif
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Failed to open network.");
return false;
}
// Set the socket to non-blocking because we're going to poll it
// every 1/10th of a second.
unsigned long ulT = 1;
#if defined(PIL)
// I think the NetSocket.c fcntl simulator routine (NetFCntl) is screwed
// up, using the wrong constants and not conforming to its own documentation.
// But for now I'll go with the flow...
if (fcntl(m_socBroadcastListen, F_SETFL, FNDELAY) == SOCKET_ERROR) {
#elif defined(IPHONE)
if (fcntl(m_socBroadcastListen, F_SETFL, fcntl(m_socBroadcastListen, F_GETFL, 0) | O_NONBLOCK) == -1) {
#else
if (ioctlsocket(m_socBroadcastListen, FIONBIO, &ulT) == SOCKET_ERROR) {
#endif
#ifdef DEBUG
HostMessageBox(TEXT("socket err: %s"), PszFromSocError());
#endif
HtMessageBox(kfMbWhiteBorder, "Comm Problem", "Network error.");
return false;
}
sockaddr_in soca;
soca.sin_family = AF_INET;
// soca.sin_addr.S_un.S_addr = htonl(m_dwIpAddress);
#ifdef IPHONE
soca.sin_addr.s_addr = htonl(INADDR_ANY);
#else
soca.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
#endif
// The server broadcasts across 4 ports so we can run up to 4 clients on
// the same machine without conflict. Loop through all the ports until
// we find a free one (one not taken already by another running client).
int i;
for (i = 0; i < 4; i++) {
soca.sin_port = htons(SERVERINFO_BROADCAST_PORT + i);
if (bind(m_socBroadcastListen, (sockaddr *)&soca, sizeof(soca)) != SOCKET_ERROR)
break;
}
#ifdef DEBUG
if (i == 4)
HostMessageBox(TEXT("bind err: %s"), PszFromSocError());
#endif
// Start timer
gtimm.AddTimer(this, 10); // Every 100ms, i.e., 1/10 second
return true;
}
void SocTransport::EndGameSearch()
{
if (m_socBroadcastListen == INVALID_SOCKET)
return;
// Stop timer
gtimm.RemoveTimer(this);
closesocket(m_socBroadcastListen);
m_socBroadcastListen = INVALID_SOCKET;
}
void SocTransport::NextGameHost()
{
m_fBlockNextGameHost = false;
}
// AdvertiseGame must clean up after itself if it fails, i.e.,
// do not assume that UnadvertiseGame will be called.
bool SocTransport::AdvertiseGame(const char *pszGameName)
{
int err;
// Remember the game name we'll be broadcasting
strncpyz(m_szGameName, (char *)pszGameName, sizeof(m_szGameName));
// Create a socket to broadcast through
m_socBroadcast = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (m_socBroadcast == INVALID_SOCKET) {
#ifdef DEBUG
HostMessageBox(TEXT("socket err: %s"), PszFromSocError());
#endif
return false;
}
int broadcast = 1;
setsockopt(m_socBroadcast, SOL_SOCKET, SO_BROADCAST, (char *)&broadcast, sizeof(broadcast));
sockaddr_in soca;
memset(&soca, 0, sizeof(soca));
soca.sin_family = AF_INET;
// soca.sin_addr.S_un.S_addr = htonl(m_dwIpAddress);
#ifdef IPHONE
soca.sin_addr.s_addr = htonl(INADDR_ANY);
#else
soca.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
#endif
err = bind(m_socBroadcast, (sockaddr *)&soca, sizeof(soca));
if (err == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("socket err: %s"), PszFromSocError());
#endif
closesocket(m_socBroadcast);
m_socBroadcast = INVALID_SOCKET;
return false;
}
// Create the socket for listening to client connection requests
m_socAcceptListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_socAcceptListen == INVALID_SOCKET) {
#ifdef DEBUG
HostMessageBox(TEXT("socket err: %s"), PszFromSocError());
#endif
if (m_socBroadcast != INVALID_SOCKET) {
closesocket(m_socBroadcast);
m_socBroadcast = INVALID_SOCKET;
}
return false;
}
// Disable nagle algorithm -- should be inherited by all accepted sockets
int nTrue = 1;
setsockopt(m_socAcceptListen, IPPROTO_TCP, TCP_NODELAY, (NetBuff)&nTrue, sizeof(int));
// Bind the listening socket to the local address and the connection port
NetAddress nad;
GetLocalNetAddress(&nad);
((sockaddr_in *)&nad)->sin_port = htons(SERVER_LISTEN_PORT);
#ifdef IPHONE
((sockaddr_in *)&nad)->sin_addr.s_addr = htons(INADDR_ANY);
#else
((sockaddr_in *)&nad)->sin_addr.S_un.S_addr = htons(INADDR_ANY);
#endif
// HostMessageBox("addr %s, fam %d, port %d", inet_ntoa(((sockaddr_in *)&nad)->sin_addr), ((sockaddr_in *)&nad)->sin_family, ntohs(((sockaddr_in *)&nad)->sin_port));
err = bind(m_socAcceptListen, (const sockaddr *)&nad, sizeof(sockaddr_in));
if (err == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("bind err: %s"), PszFromSocError());
#endif
if (m_socBroadcast != INVALID_SOCKET) {
closesocket(m_socBroadcast);
m_socBroadcast = INVALID_SOCKET;
}
closesocket(m_socAcceptListen);
m_socAcceptListen = INVALID_SOCKET;
return false;
}
// Prepare for client connections
err = listen(m_socAcceptListen, SOMAXCONN);
if (err == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("listen err: %s"), PszFromSocError());
#endif
if (m_socBroadcast != INVALID_SOCKET) {
closesocket(m_socBroadcast);
m_socBroadcast = INVALID_SOCKET;
}
closesocket(m_socAcceptListen);
m_socAcceptListen = INVALID_SOCKET;
return false;
}
if (m_socBroadcast != INVALID_SOCKET) {
// Start timer
gtimm.AddTimer(this, 50); // Every 500ms, i.e., 1/2 second
// Broadcast game availability immediately
OnTimer(0);
}
return true;
}
void SocTransport::UnadvertiseGame()
{
// Close connection accepting socket
closesocket(m_socAcceptListen);
m_socAcceptListen = INVALID_SOCKET;
// Close broadcast socket
if (m_socBroadcast != INVALID_SOCKET) {
closesocket(m_socBroadcast);
m_socBroadcast = INVALID_SOCKET;
// Stop timer
gtimm.RemoveTimer(this);
}
}
void SocTransport::OnTimer(long tCurrent)
{
// If we're Advertising broadcast a SERVERINFO message
if (m_socBroadcast != INVALID_SOCKET) {
sockaddr_in soca;
soca.sin_family = AF_INET;
#ifdef IPHONE
soca.sin_addr.s_addr = INADDR_BROADCAST;
#else
soca.sin_addr.S_un.S_addr = INADDR_BROADCAST;
#endif
ServerInfoNetMessage nm(m_szGameName);
MpTrace("> %s", PszFromNetMessage(&nm));
// Broadcast to 4 ports to support running 4 clients on the same machine
for (int i = 0; i < 4; i++) {
soca.sin_port = htons(SERVERINFO_BROADCAST_PORT + i);
if (sendto(m_socBroadcast, (char *)&nm, sizeof(nm), 0, (sockaddr *)&soca, sizeof(soca)) == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("sendto err: %s"), PszFromSocError());
#endif
}
}
}
// If we're searching for hosts check for receipt of a SERVERINFO message
if (m_socBroadcastListen != INVALID_SOCKET) {
NetAddress nad;
#ifdef IPHONE
socklen_t cbSocaServer = sizeof(nad);
#else
int cbSocaServer = sizeof(nad);
#endif
ServerInfoNetMessage sinm;
if (recvfrom(m_socBroadcastListen, (char *)&sinm, sizeof(sinm), 0, (sockaddr *)&nad, &cbSocaServer) == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK && err != WSAETIMEDOUT) {
#ifdef DEBUG
HostMessageBox(TEXT("recvfrom err: %s"), PszFromSocError(err));
// UNDONE: If we discover any errors that would cause us to want to abort the
// game search operation...
// if (m_ptcb != NULL)
// m_ptcb->OnTransportError(ktraeGameSearchFailed);
#endif
return;
}
} else {
MpTrace("< GAMEHOSTFOUND");
if (m_fBlockNextGameHost)
return;
// Fill in a NetAddress ourselves so we can be sure no random numbers are present
NetAddress nadT;
memset(&nadT, 0, sizeof(nadT));
// HACK: this is the port the game is going to want to connect to
((sockaddr_in *)&nadT)->sin_port = htons(SERVER_LISTEN_PORT);
((sockaddr_in *)&nadT)->sin_family = ((sockaddr_in *)&nad)->sin_family;
((sockaddr_in *)&nadT)->sin_addr = ((sockaddr_in *)&nad)->sin_addr;
if (m_ptcb != NULL) {
m_fBlockNextGameHost = true;
m_ptcb->OnGameHostFound(&nadT);
}
}
}
}
bool SocTransport::CanSpecifyAddress()
{
return true;
}
bool ValidateIpAddress(const char *psz) secComm;
bool ValidateIpAddress(const char *psz)
{
dword dwIpAddress = inet_addr(psz);
if (dwIpAddress == INADDR_NONE) {
HtMessageBox(kfMbWhiteBorder, "Input Error", "Invalid Internet address.");
return false;
}
return true;
}
bool SocTransport::SpecifyAddress(NetAddress *pnad)
{
#if 0
static char *s_apszChars[] = {
"ABCDEFGHIJKLM",
"NOPQRSTUVWXYZ",
"0123456789.-",
};
#else
static char *s_apszChars[] = {
"123",
"456",
"789",
".0"
};
#endif
char szIpAddress[32];
if (!DoInputPanelForm(s_apszChars, ARRAYSIZE(s_apszChars), "Address:", "", szIpAddress, sizeof(szIpAddress), ValidateIpAddress))
return false;
dword nlIpAddress = inet_addr(szIpAddress);
sockaddr_in *psoca = (sockaddr_in *)pnad;
psoca->sin_family = AF_INET;
psoca->sin_port = htons(SERVER_LISTEN_PORT);
#ifdef IPHONE
psoca->sin_addr.s_addr = nlIpAddress;
#else
psoca->sin_addr.S_un.S_addr = nlIpAddress;
#endif
return true;
}
//---------------------------------------------------------------------------
// SocConnection implementation
SocConnection::SocConnection()
{
m_soc = INVALID_SOCKET;
m_fListening = false;
m_fDisconnecting = false;
}
SocConnection::SocConnection(SOCKET soc)
{
m_soc = soc;
m_fListening = false;
m_fDisconnecting = false;
}
SocConnection::~SocConnection()
{
if (m_soc != INVALID_SOCKET)
closesocket(m_soc);
gptra->RemoveConnection(this);
}
bool SocConnection::Poll()
{
if (m_fDisconnecting) {
m_fDisconnecting = false;
if (m_pccb != NULL)
m_pccb->OnDisconnect(this);
return false;
}
if (m_soc == INVALID_SOCKET)
return false;
// Test if any incoming data is pending
fd_set fds;
FD_ZERO(&fds);
FD_SET(m_soc, &fds);
TIMEVAL tvTimeout;
tvTimeout.tv_sec = 0;
tvTimeout.tv_usec = 0;
fd_set fdsDummy;
FD_ZERO(&fdsDummy);
// MpTrace("m_soc = %d, fds = %ld", (UInt16)m_soc, (UInt32)fds);
int nSelected = select(((int)m_soc) + 1, &fds, &fdsDummy, &fdsDummy, &tvTimeout);
if (nSelected == SOCKET_ERROR) {
#ifdef DEBUG
HostMessageBox(TEXT("select err: %s"), PszFromSocError());
#endif
return false;
}
if (nSelected == 1) {
NetMessage nm;
int cb = recv(m_soc, (char *)&nm, sizeof(NetMessage), 0);
// MpTrace("recv: cb %d", cb);
if (cb != sizeof(NetMessage)) {
HandleRecvError();
return false;
}
int cbT = BigWord(nm.cb);
NetMessage *pnm = (NetMessage *)new byte[cbT];
if (pnm == NULL) {
// Data is still pending but we can't do anything about it.
// Follow the usual course of action (HandleRecvError will close the Connection)
HandleRecvError();
return false;
}
memcpy(pnm, &nm, sizeof(NetMessage));
int cbRemaining = cbT - sizeof(NetMessage);
// UNDONE: this now is a blocking operation until all of the message is
// received. Better would be to accumulate the pieces of the message in
// a temp buffer (SocConnection member) until it is complete, in a non-
// blocking fashion.
byte *pbT = (byte *)(pnm + 1);
while (cbRemaining != 0) {
cbT = recv(m_soc, (char *)pbT, cbRemaining, 0);
if (cbT == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK) {
delete[] pnm;
HandleRecvError();
return false;
}
}
pbT += cbT;
cbRemaining -= cbT;
}
if (m_pccb != NULL) {
// Before calling OnReceive, order in native byte order
NetMessageByteOrderSwap(pnm, false);
MpTrace("< %s", PszFromNetMessage(pnm));
m_pccb->OnReceive(this, pnm);
}
delete[] pnm;
}
return true;
}
void SocConnection::HandleRecvError()
{
// Pretty much any error is fatal to a Connection but we'll start
// by casing them out one by one so everything is considered
int err = WSAGetLastError();
// On CE.NET when a peer terminates ungracefully recv will return partial
// messages. We detect this and treat it like any other fatal error.
if (err < WSABASEERR)
err = 0;
switch (err) {
case 0: // When the peer gracefully closes its socket
case WSAECONNRESET: // When the peer crashes, etc (ungraceful termination)
case WSAECONNABORTED:
case netErrSocketClosedByRemote: // Couldn't map this to WSACONRESET or WSACONABORTED because they're already mapped to other Palm errors
AsyncDisconnect();
break;
// Treat all errors as fatal to the Connection and Disconnect
default:
#ifdef DEBUG
HostMessageBox(TEXT("recv err: %s"), PszFromSocError(err));
#endif
AsyncDisconnect();
break;
}
}
void SocConnection::HandleSendError()
{
// Pretty much any error is fatal to a Connection but we'll start
// by casing them out one by one so everything is considered
int err = WSAGetLastError();
switch (err) {
case WSAECONNRESET:
AsyncDisconnect();
break;
// Treat all errors as fatal to the Connection and Disconnect
default:
#ifdef DEBUG
HostMessageBox(TEXT("send err: %s"), PszFromSocError(err));
#endif
AsyncDisconnect();
break;
}
}
bool SocConnection::AsyncSend(NetMessage *pnm)
{
if (m_soc == INVALID_SOCKET)
return false;
MpTrace("> %s", PszFromNetMessage(pnm));
// Before sending, order in network byte order
int cb = pnm->cb; // nab this before byte-swapping it!
NetMessageByteOrderSwap(pnm, true);
// UNDONE: this is synchronous
int cbActual = send(m_soc, (NetBuff)pnm, cb, 0);
// Swap it back in case of reuse!
NetMessageByteOrderSwap(pnm, false);
if (cbActual != cb) {
HandleSendError();
return false;
}
return true;
}
void SocConnection::AsyncDisconnect()
{
if (m_soc == INVALID_SOCKET)
return;
shutdown(m_soc, SD_SEND);
closesocket(m_soc);
m_soc = INVALID_SOCKET;
// Callers assume that the disconnect notification will be async
// so we do a little extra work to satisfy this.
m_fDisconnecting = true;
}
// Error strings for Socket errors
#ifdef DEBUG
const TCHAR *PszFromSocError()
{
return PszFromSocError(WSAGetLastError());
}
#endif
} // namespace wi