#include "global.h" #include "CrashHandlerNetworking.h" #include "RageThreads.h" #include "RageLog.h" #include "RageThreads.h" #include "RageTimer.h" #include "RageUtil.h" #include "Foreach.h" #if defined(WINDOWS) #include #include #pragma comment(lib, "wsock32.lib") #pragma comment(lib, "ws2_32.lib") #endif /* * This has an arch-like abstraction layout, since it's intended to become one. * It'll be moved out of here as things get polished further. The Winsock and * BSD implementations will be kept separate, since while they look similar, * they're different in the details, especially when you want to cleanly support * cancellation. * * Design notes: * * - Allow cancellation at any time. The only thing more annoying than a laggy * network, is an application that won't immediately respond to cancelling. * (This is even more important here, since we're using this after a crash.) * - Assume some operations and/or some architectures are going to have problems * doing everything nonblockingly, and do everything in a thread. This also allows * more complex network interactions, since it doesn't need to maintain an equally * complex state machine. * - When an operation is cancelled or an error occurs, all operations until the * next call to Close() become no-ops. This allows lenient error checking; it's * guaranteed that further calls to Read or Write will not reset the error state. * - The only function that may be called from another thread without serialization is * Cancel(). * * All calls are blocking, except for: * * - Cancel(), which is always nonblocking. The operation in progress, if any, * will be aborted immediately. (The only exception here is incomplete * implementations, which may block); * - Close(), if Shutdown() was called first, since the necessary blocking occurs * during Shutdown; * - Close(), if an error or cancellation occurred. * * Accepting incoming TCP connections is beyond the immediate scope of this interface * (that would involve a second listener class, which would be a factory for NetworkStream). * UDP is probably within the scope of this class, but there are some outstanding design * issues and it's beyond the work I'm doing right now. */ class NetworkStream { public: enum ConnectionType { CONN_TCP, CONN_UDP, }; NetworkStream() { } virtual ~NetworkStream() { } /* * Open a connection. Must be in STATE_IDLE. */ virtual void Open( const RString &sHost, int iPort, ConnectionType ct = CONN_TCP ) = 0; /* * Close down a connection. Returns to STATE_IDLE. */ virtual void Close() = 0; /* * Wait for all sent data to be flushed, and shut down the connection. */ virtual void Shutdown() = 0; /* * Read data. Block until any data is received, then return all data * available. Return the number of bytes read. The return value will * always be >= 0, unless an error or cancellation occured. */ virtual int Read( void *pBuffer, size_t iSize ) = 0; /* * Write data to the socket. (Design note: we always write all of the * data unless an error or cancellation occurs, and those states are checked * with GetState(). If that happens, the number of bytes written is * meaningless, since it may have simply been buffered and never sent. So, * this function returns no value.) */ virtual void Write( const void *pBuffer, size_t iSize ) = 0; /* * Cancel the connection. This operation can clear an error state, aborts * any blocking calls, never fails, and will always result in the socket * being in STATE_CANCELLED. * * This is true even if the state was STATE_IDLE. For example, the user * thread may be closing one connection on this object and opening another, * and the UI thread may call Cancel() in between these operations. * * This operation is threadsafe: a socket can be cancelled from any thread * while another thread is reading or writing. This is the only function * that can be called without serialization. */ virtual void Cancel() = 0; enum State { /* The stream is closed, and is ready to be opened. */ STATE_IDLE, /* The stream is connected and able to send and receive data. */ STATE_CONNECTED, /* The stream has been shut down on either end (either an EOF from * the other end, or shutdown() being called). */ STATE_SHUTDOWN, /* When in CANCELLED, all blocking calls fail immediately; this state must be * cleared explicitly by calling Close(). */ STATE_CANCELLED, STATE_ERROR }; State GetState() const { return m_State; } RString GetError() const { return m_sError; } protected: State m_State; RString m_sError; }; class NetworkStream_Win32: public NetworkStream { public: NetworkStream_Win32(); ~NetworkStream_Win32(); void Open( const RString &sHost, int iPort, ConnectionType ct = CONN_TCP ); void Shutdown(); void Close(); int Read( void *pBuffer, size_t iSize ); void Write( const void *pBuffer, size_t iSize ); void Cancel(); private: int WaitForCompletionOrCancellation( int iEvent ); void SetError( const RString &sError ); static RString WinSockErrorToString( int iError ); SOCKET m_Socket; HANDLE m_hResolve; HWND m_hResolveHwnd; /* This event is signalled on cancellation, to wake us up if we're blocking. */ HANDLE m_hCompletionEvent; RString m_sHost; int m_iPort; RageMutex m_Mutex; }; NetworkStream *CreateNetworkStream() { static bool bInitted = false; if( !bInitted ) { bInitted = true; WSADATA WSAData; WORD iVersionRequested = MAKEWORD(2,0); if( WSAStartup(iVersionRequested, &WSAData) != 0 ) return NULL; } return new NetworkStream_Win32; } /* WinSock implementation of NetworkStream. */ NetworkStream_Win32::NetworkStream_Win32(): m_Mutex( "NetworkTCPSocket" ) { m_iPort = -1; m_State = STATE_IDLE; m_Socket = NULL; #if defined(WINDOWS) m_hResolve = NULL; m_hResolveHwnd = NULL; m_hCompletionEvent = CreateEvent( NULL, true, false, NULL ); #endif } NetworkStream_Win32::~NetworkStream_Win32() { Close(); CloseHandle( m_hCompletionEvent ); } /* Wait for the specified network event to occur, or cancellation, whichever * happens first. On cancellation, return -1; on error, return the error * code; on success, return 0. */ int NetworkStream_Win32::WaitForCompletionOrCancellation( int iEvent ) { while(1) { int iRet = WaitForSingleObject( m_hCompletionEvent, INFINITE ); if( iRet != WAIT_OBJECT_0 ) continue; m_Mutex.Lock(); /* This will reset the event. Do this while we hold the lock. */ WSANETWORKEVENTS events; WSAEnumNetworkEvents( m_Socket, m_hCompletionEvent, &events ); /* Was the event signalled due to cancellation? */ if( m_State == STATE_CANCELLED ) { m_Mutex.Unlock(); return -1; } m_Mutex.Unlock(); /* If the event didn't actually occur, keep waiting. */ if( (events.lNetworkEvents & (1<h_addr_list[0]; addr.sin_family = PF_INET; addr.sin_port = htons( (uint16_t) iPort ); m_Mutex.Lock(); m_Socket = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP ); if( m_Socket == INVALID_SOCKET ) { int iError = WSAGetLastError(); SetError( ssprintf("Error creating socket: %s", WinSockErrorToString(iError).c_str() ) ); return; } /* Set up the completion event to be signalled when these events occur. This * also sets the socket to nonblocking. */ WSAEventSelect( m_Socket, m_hCompletionEvent, FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ); // fcntl( m_Socket, O_NONBLOCK, 1 ); /* Start opening the connection. */ int iResult = connect(m_Socket, (SOCKADDR *) &addr, sizeof(addr)); m_Mutex.Unlock(); /* We expect EINPROGRESS/WSAEWOULDBLOCK. */ if( iResult == SOCKET_ERROR ) { /* Block until the connection attempt completes. */ int iError = WSAGetLastError(); if( iError == WSAEWOULDBLOCK ) iError = WaitForCompletionOrCancellation( FD_CONNECT_BIT ); if( iError ) { SetError( ssprintf("Couldn't connect: %s", WinSockErrorToString(iError).c_str() ) ); return; } } } m_State = STATE_CONNECTED; } void NetworkStream_Win32::Close() { if( m_State == STATE_IDLE ) return; /* If we have an active, stable connection, make sure we flush any data * completely before closing. If you don't want to do this, call Cancel() * first. */ Shutdown(); m_Mutex.Lock(); if( m_State == STATE_CANCELLED ) return; if( m_Socket != INVALID_SOCKET ) closesocket( m_Socket ); m_Socket = INVALID_SOCKET; m_State = STATE_IDLE; m_Mutex.Unlock(); } void NetworkStream_Win32::Shutdown() { /* If we're not in a normal, stable state (eg. an error occurred, or we were * cancelled), don't wait to flush. */ if( m_State != STATE_CONNECTED ) return; shutdown( m_Socket, SD_BOTH ); m_State = STATE_SHUTDOWN; } void NetworkStream_Win32::Cancel() { m_Mutex.Lock(); /* Mark cancellation. */ m_State = STATE_CANCELLED; /* If resolving, abort the resolve. */ if( m_hResolve != NULL ) { /* When we cancel the request, no message at all will be sent to the window, * so we need to do it ourself to inform it that it was cancelled. Be sure * to only do this on successful cancel. */ if( WSACancelAsyncRequest(m_hResolve) == 0 ) PostMessage( m_hResolveHwnd, WM_USER+1, 0, 0 ); } /* Break out if we're waiting in WaitForCompletionOrCancellation(). */ SetEvent( m_hCompletionEvent ); m_Mutex.Unlock(); } int NetworkStream_Win32::Read( void *pBuffer, size_t iSize ) { if( m_State != STATE_CONNECTED ) return 0; char *p = (char *) pBuffer; int iRead = 0; while( iSize > 0 ) { int iRet = recv( m_Socket, p, iSize, 0 ); if( iRet > 0 ) { p += iRet; iSize -= iRet; iRead += iRet; continue; } if( iRet == 0 ) { /* We hit EOF. */ break; } int iError = WSAGetLastError(); if( iError == WSAEWOULDBLOCK ) { /* There's no date to read. If we've already received some data, return it. */ if( iRead > 0 ) break; iError = WaitForCompletionOrCancellation( FD_READ_BIT ); if( iError == 0 ) continue; } /* If the other side closed the connection, just return EOF. */ if( iError == WSAECONNRESET ) break; /* If we're cancelled or hit an error while reading, do return the data we managed * to get. Be sure not to overwrite CANCELLED with ERROR. */ SetError( ssprintf("Error reading: %s", WinSockErrorToString(iError).c_str() ) ); break; } return iRead; } void NetworkStream_Win32::Write( const void *pBuffer, size_t iSize ) { if( m_State != STATE_CONNECTED ) return; const char *p = (const char *) pBuffer; while( iSize > 0 ) { int iRet = send( m_Socket, p, iSize, 0 ); ASSERT( iRet != 0 ); if( iRet > 0 ) { p += iRet; iSize -= iRet; continue; } int iError = WSAGetLastError(); if( iError == WSAEWOULDBLOCK ) { iError = WaitForCompletionOrCancellation( FD_WRITE_BIT ); if( iError == 0 ) continue; } SetError( ssprintf("Error writing: %s", WinSockErrorToString(iError).c_str() ) ); return; } } /* * Send a set of data over HTTP, as a POST form. */ NetworkPostData::NetworkPostData(): m_Mutex( "NetworkPostData" ) { m_pStream = CreateNetworkStream(); } NetworkPostData::~NetworkPostData() { delete m_pStream; } /* Create a MIME multipart data block from the given set of fields. */ void NetworkPostData::CreateMimeData( const map &mapNameToData, RString &sOut, RString &sMimeBoundaryOut ) { /* Find a non-conflicting mime boundary. */ while(1) { sMimeBoundaryOut = ssprintf( "--%08i", rand() ); FOREACHM_CONST( RString, RString, mapNameToData, d ) if( d->second.find(sMimeBoundaryOut) != RString::npos ) continue; break; } FOREACHM_CONST( RString, RString, mapNameToData, d ) { sOut += "--" + sMimeBoundaryOut + "\r\n"; sOut += ssprintf( "Content-Disposition: form-data; name=\"%s\"\r\n", d->first.c_str() ); sOut += "\r\n"; sOut += d->second; sOut += "\r\n"; } if( sOut.size() ) sOut += "--" + sMimeBoundaryOut + "--\r\n"; } void NetworkPostData::HttpThread() { RString sData, sMimeBoundary; CreateMimeData( m_Data, sData, sMimeBoundary ); // Stick to HTTP/1.0, since the protocol is simpler. RString sBuf = ssprintf( "%s %s HTTP/1.0\r\n" "Accept: */*\r\n" "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)\r\n" "Host: %s\r\n" "Cache-Control: no-cache\r\n", sData.size()? "POST":"GET", m_sPath.c_str(), m_sHost.c_str() ); if( sData.size() ) { // sBuf += "Content-Type: application/x-www-form-urlencoded\r\n" sBuf += "Content-Type: multipart/form-data; boundary=" + sMimeBoundary + "\r\n"; sBuf += ssprintf( "Content-Length: %i\r\n", sData.size() ); } sBuf += "\r\n"; if( sData.size() ) sBuf += sData; /* The "progress" is currently faked; it shows when we've connected, and when * we've received data. We send and receive too little data to do more. */ SetProgress( 0 ); /* * Begin connecting. */ m_pStream->Open( m_sHost, m_iPort ); SetProgress( 0.25f ); /* Send the form. */ m_pStream->Write( sBuf.data(), sBuf.size() ); /* Read the result. */ RString sResult; while( m_pStream->GetState() == NetworkStream::STATE_CONNECTED ) { sBuf.clear(); void *p = sBuf.GetBuffer( 1024 ); int iGot = m_pStream->Read( p, 1024 ); if( iGot >= 0 ) sBuf.ReleaseBuffer( iGot ); if( iGot <= 0 ) break; sResult += sBuf; } SetProgress( 1.0f ); /* Parse the results. */ int iStart = 0, iSize = -1; map mapHeaders; while( 1 ) { split( sResult, "\n", iStart, iSize, false ); if( iStart == (int) sResult.size() ) break; RString sLine = sResult.substr( iStart, iSize ); StripCrnl( sLine ); if( sLine.empty() ) { m_sResult = sResult.substr( iStart+iSize+1 ); break; } } m_bFinished = true; } void NetworkPostData::Start( const RString &sHost, int iPort, const RString &sPath ) { m_bFinished = false; m_sHost = sHost; m_iPort = iPort; m_sPath = sPath; m_sStatus.clear(); m_fProgress = 0; m_Thread.SetName( "HTTP thread" ); m_Thread.Create( HttpThread_Start, this ); } void NetworkPostData::Cancel() { m_pStream->Cancel(); m_Thread.Wait(); } bool NetworkPostData::IsFinished() { if( !m_bFinished ) return false; m_Thread.Wait(); m_bFinished = false; return true; } RString NetworkPostData::GetStatus() const { LockMut( m_Mutex ); return m_sStatus; } float NetworkPostData::GetProgress() const { LockMut( m_Mutex ); return m_fProgress; } RString NetworkPostData::GetError() const { return m_pStream->GetError(); } void NetworkPostData::SetProgress( float fProgress ) { m_Mutex.Lock(); m_fProgress = fProgress; m_Mutex.Unlock(); } void NetworkPostData::SetData( const RString &sKey, const RString &sData ) { m_Data[sKey] = sData; } /* * (c) 2006 Glenn Maynard * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, and/or sell copies of the Software, and to permit persons to * whom the Software is furnished to do so, provided that the above * copyright notice(s) and this permission notice appear in all copies of * the Software and that both the above copyright notice(s) and this * permission notice appear in supporting documentation. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */