diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a81786d3..8657498d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ if(QUIC_BUILD_TOOLS) add_subdirectory(src/tools/interop) add_subdirectory(src/tools/interopserver) add_subdirectory(src/tools/ping) + add_subdirectory(src/tools/post) add_subdirectory(src/tools/reach) add_subdirectory(src/tools/sample) add_subdirectory(src/tools/spin) diff --git a/src/tools/interopserver/InteropServer.cpp b/src/tools/interopserver/InteropServer.cpp index ca1e315e6..caa35903c 100644 --- a/src/tools/interopserver/InteropServer.cpp +++ b/src/tools/interopserver/InteropServer.cpp @@ -14,6 +14,7 @@ Abstract: const QUIC_API_TABLE* MsQuic; QUIC_SEC_CONFIG* SecurityConfig; const char* RootFolderPath; +const char* UploadFolderPath; const QUIC_BUFFER SupportedALPNs[] = { { sizeof("hq-27") - 1, (uint8_t*)"hq-27" }, @@ -29,7 +30,8 @@ PrintUsage() printf(" interopserver.exe -listen: -root:" " [-thumbprint:] [-name:]" " [-file: AND -key:]" - " [-port:<####> (def:%u)] [-retry:<0/1> (def:%u)]\n\n", + " [-port:<####> (def:%u)] [-retry:<0/1> (def:%u)]" + " [-upload:]\n\n", DEFAULT_QUIC_HTTP_SERVER_PORT, DEFAULT_QUIC_HTTP_SERVER_RETRY); printf("Examples:\n"); @@ -67,6 +69,7 @@ main( EXIT_ON_FAILURE(QuicForceRetry(MsQuic, true)); printf("Enabling forced RETRY on server.\n"); } + TryGetValue(argc, argv, "upload", &UploadFolderPath); // // Required parameters. @@ -128,18 +131,23 @@ main( // HttpRequest // -HttpRequest::HttpRequest(HttpConnection *connection, HQUIC stream) : +HttpRequest::HttpRequest(HttpConnection *connection, HQUIC stream, bool Unidirectional) : Connection(connection), QuicStream(stream), File(nullptr), Shutdown(false), WriteHttp11Header(false) { - MsQuic->SetCallbackHandler(QuicStream, (void*)QuicCallbackHandler, this); + MsQuic->SetCallbackHandler( + QuicStream, + Unidirectional ? + (void*)QuicUnidiCallbackHandler : + (void*)QuicBidiCallbackHandler, + this); Connection->AddRef(); } HttpRequest::~HttpRequest() { if (File) { - fclose(File); + fclose(File); // TODO - If POST, abandon/delete file as it wasn't finished. } MsQuic->StreamClose(QuicStream); Connection->Release(); @@ -177,16 +185,16 @@ HttpRequest::Process() return; } - char fullFilePath[256]; - strcpy(fullFilePath, RootFolderPath); + char FullFilePath[256]; + strcpy(FullFilePath, RootFolderPath); if (strcmp("/", PathStart) == 0) { - strcat(fullFilePath, "/index.html"); + strcat(FullFilePath, "/index.html"); } else { - strcat(fullFilePath, PathStart); + strcat(FullFilePath, PathStart); } printf("[%s] GET '%s'\n", GetRemoteAddr(MsQuic, QuicStream).Address, PathStart); - File = fopen(fullFilePath, "rb"); // In case of failure, SendData still works. + File = fopen(FullFilePath, "rb"); // In case of failure, SendData still works. SendData(); } @@ -252,9 +260,71 @@ HttpRequest::SendData() } } +bool +HttpRequest::ReceiveUniDiData( + _In_ const QUIC_BUFFER* Buffers, + _In_ uint32_t BufferCount + ) +{ + if (UploadFolderPath == nullptr) { + printf("[%s] Server not configured for POST!\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + + uint32_t SkipLength = 0; + if (File == nullptr) { + const QUIC_BUFFER* FirstBuffer = Buffers; + if (FirstBuffer->Length < 5 || + _strnicmp((const char*)FirstBuffer->Buffer, "post ", 5) != 0) { + printf("[%s] Invalid post prefix\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + + char* FileName = (char*)FirstBuffer->Buffer + 5; + char* FileNameEnd = strstr(FileName, "\r\n"); + if (FileNameEnd == nullptr) { + printf("[%s] Invalid post suffix\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + *FileNameEnd = '\0'; // We shouldn't be writing to the buffer. Don't imitate this. + + if (strstr(FileName, "..") != nullptr) { + printf("[%s] '..' found\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + + char FullFilePath[256]; + if (snprintf(FullFilePath, sizeof(FullFilePath), "%s/%s", UploadFolderPath, FileName) < 0) { + printf("[%s] Invalid path\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + + printf("[%s] POST '%s'\n", GetRemoteAddr(MsQuic, QuicStream).Address, FileName); + File = fopen(FullFilePath, "wb"); + if (!File) { + printf("[%s] Failed to open file\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + + FileNameEnd += 2; // Skip "\r\n" + SkipLength = (uint32_t)((uint8_t*)FileNameEnd - FirstBuffer->Buffer); + } + + for (uint32_t i = 0; i < BufferCount; ++i) { + uint32_t DataLength = Buffers[i].Length - SkipLength; + if (fwrite(Buffers[i].Buffer + SkipLength, 1, DataLength, File) < DataLength) { + printf("[%s] Failed to write file\n", GetRemoteAddr(MsQuic, QuicStream).Address); + return false; + } + SkipLength = 0; + } + + return true; +} + QUIC_STATUS QUIC_API -HttpRequest::QuicCallbackHandler( +HttpRequest::QuicBidiCallbackHandler( _In_ HQUIC Stream, _In_opt_ void* Context, _Inout_ QUIC_STREAM_EVENT* Event @@ -295,3 +365,37 @@ HttpRequest::QuicCallbackHandler( return QUIC_STATUS_SUCCESS; } + +_Function_class_(QUIC_STREAM_CALLBACK) +QUIC_STATUS +QUIC_API +HttpRequest::QuicUnidiCallbackHandler( + _In_ HQUIC Stream, + _In_opt_ void* Context, + _Inout_ QUIC_STREAM_EVENT* Event + ) +{ + auto pThis = (HttpRequest*)Context; + switch (Event->Type) { + case QUIC_STREAM_EVENT_RECEIVE: + if (!pThis->ReceiveUniDiData(Event->RECEIVE.Buffers, Event->RECEIVE.BufferCount)) { + pThis->Abort(1); // BUG - Seems like we continue to get receive callbacks! + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + if (pThis->File) { + fclose(pThis->File); + pThis->File = nullptr; + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + printf("[%s] Peer abort (0x%llx)\n", + GetRemoteAddr(MsQuic, Stream).Address, + Event->PEER_SEND_ABORTED.ErrorCode); + break; + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + delete pThis; + break; + } + return QUIC_STATUS_SUCCESS; +} diff --git a/src/tools/interopserver/InteropServer.h b/src/tools/interopserver/InteropServer.h index 6f78e8ffc..863059df1 100644 --- a/src/tools/interopserver/InteropServer.h +++ b/src/tools/interopserver/InteropServer.h @@ -66,7 +66,7 @@ struct HttpSendBuffer { struct HttpConnection; struct HttpRequest { - HttpRequest(HttpConnection *connection, HQUIC stream); + HttpRequest(HttpConnection *connection, HQUIC stream, bool Unidirectional); private: HttpConnection *Connection; HQUIC QuicStream; @@ -85,10 +85,22 @@ private: } void Process(); void SendData(); + bool ReceiveUniDiData( + _In_ const QUIC_BUFFER* Buffers, + _In_ uint32_t BufferCount + ); static QUIC_STATUS QUIC_API - QuicCallbackHandler( + QuicBidiCallbackHandler( + _In_ HQUIC Stream, + _In_opt_ void* Context, + _Inout_ QUIC_STREAM_EVENT* Event + ); + static + QUIC_STATUS + QUIC_API + QuicUnidiCallbackHandler( _In_ HQUIC Stream, _In_opt_ void* Context, _Inout_ QUIC_STREAM_EVENT* Event @@ -126,14 +138,10 @@ private: HttpConnection *pThis = (HttpConnection*)Context; switch (Event->Type) { case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: - if (Event->PEER_STREAM_STARTED.Flags & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL) { - MsQuic->SetCallbackHandler( - Event->PEER_STREAM_STARTED.Stream, - (void*)HttpUnidirectionalStreamCallback, - nullptr); - } else { - new HttpRequest(pThis, Event->PEER_STREAM_STARTED.Stream); - } + new HttpRequest( + pThis, + Event->PEER_STREAM_STARTED.Stream, + Event->PEER_STREAM_STARTED.Flags & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL); break; case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: pThis->Release(); @@ -141,24 +149,6 @@ private: } return QUIC_STATUS_SUCCESS; } - static - _Function_class_(QUIC_STREAM_CALLBACK) - QUIC_STATUS - HttpUnidirectionalStreamCallback( - _In_ HQUIC Stream, - _In_opt_ void* /* Context */, - _Inout_ QUIC_STREAM_EVENT* Event - ) - { - if (Event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE) { - // - // Don't care about anything else on the stream except closing it in - // resposne to shutdown complete. - // - MsQuic->StreamClose(Stream); - } - return QUIC_STATUS_SUCCESS; - } }; struct HttpServer { diff --git a/src/tools/post/CMakeLists.txt b/src/tools/post/CMakeLists.txt new file mode 100644 index 000000000..746776980 --- /dev/null +++ b/src/tools/post/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +set(SOURCES + post.cpp +) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QUIC_CXX_FLAGS}") + +add_executable(quicpost ${SOURCES}) + +target_link_libraries(quicpost msquic platform) + +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") + target_link_libraries(quicpost + ws2_32 schannel ntdll bcrypt ncrypt crypt32 iphlpapi advapi32) +endif() diff --git a/src/tools/post/post.cpp b/src/tools/post/post.cpp new file mode 100644 index 000000000..16e168087 --- /dev/null +++ b/src/tools/post/post.cpp @@ -0,0 +1,203 @@ +/*++ + + Copyright (c) Microsoft Corporation. + Licensed under the MIT License. + +Abstract: + + Very Simple QUIC HTTP 0.9 POST client. + +--*/ + +#define _CRT_SECURE_NO_WARNINGS 1 + +#include + +extern "C" void QuicTraceRundown(void) { } + +#define IO_SIZE (128 * 1024) + +#define POST_HEADER_FORMAT "POST %s\r\n" + +#define EXIT_ON_FAILURE(x) do { \ + auto _Status = x; \ + if (QUIC_FAILED(_Status)) { \ + printf("%s:%d %s failed!\n", __FILE__, __LINE__, #x); \ + exit(1); \ + } \ +} while (0); + +#define ALPN_BUFFER(str) { sizeof(str) - 1, (uint8_t*)str } +const QUIC_BUFFER ALPNs[] = { + ALPN_BUFFER("hq-27"), + ALPN_BUFFER("hq-25") +}; + +const QUIC_API_TABLE* MsQuic; +const uint32_t CertificateValidationFlags = QUIC_CERTIFICATE_FLAG_DISABLE_CERT_VALIDATION; +uint16_t Port = 4433; +const char* ServerName = "localhost"; +const char* FilePath = nullptr; +FILE* File = nullptr; +QUIC_EVENT SendReady; +bool TransferCanceled = false; + +_IRQL_requires_max_(PASSIVE_LEVEL) +_Function_class_(QUIC_CONNECTION_CALLBACK) +QUIC_STATUS +QUIC_API +ConnectionHandler( + _In_ HQUIC Connection, + _In_opt_ void* /* Context */, + _Inout_ QUIC_CONNECTION_EVENT* Event + ) +{ + switch (Event->Type) { + case QUIC_CONNECTION_EVENT_CONNECTED: + printf("Connected\n"); + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + printf("Transport Shutdown 0x%x\n", Event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status); + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: + printf("Peer Shutdown 0x%llx\n", Event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode); + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: + //printf("Shutdown Complete\n"); + MsQuic->ConnectionClose(Connection); + break; + default: + break; + } + return QUIC_STATUS_SUCCESS; +} + +_IRQL_requires_max_(PASSIVE_LEVEL) +_Function_class_(QUIC_STREAM_CALLBACK) +QUIC_STATUS +QUIC_API +StreamHandler( + _In_ HQUIC Stream, + _In_opt_ void* /* Context */, + _Inout_ QUIC_STREAM_EVENT* Event + ) +{ + switch (Event->Type) { + case QUIC_STREAM_EVENT_SEND_COMPLETE: + if (Event->SEND_COMPLETE.Canceled) { + TransferCanceled = true; + printf("Send canceled!\n"); + } + QuicEventSet(SendReady); + break; + case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: + printf("Peer stream recv abort (0x%llx)\n", Event->PEER_RECEIVE_ABORTED.ErrorCode); + break; + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + MsQuic->ConnectionShutdown(Stream, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0); + MsQuic->StreamClose(Stream); + break; + default: + break; + } + return QUIC_STATUS_SUCCESS; +} + +int +QUIC_MAIN_EXPORT +main( + _In_ int argc, + _In_reads_(argc) _Null_terminated_ char* argv[] + ) +{ + if (argc < 2 || !TryGetValue(argc, argv, "file", &FilePath)) { + printf("Usage: quicpost.exe [-server:] [-ip:] [-port:] -file:\n"); + exit(1); + } + + TryGetValue(argc, argv, "server", &ServerName); + TryGetValue(argc, argv, "port", &Port); + + QuicPlatformSystemLoad(); + QuicPlatformInitialize(); + + File = fopen(FilePath, "rb"); + if (File == nullptr) { + printf("Failed to open file!\n"); + exit(1); + } + + const char* FileName = strrchr(FilePath, '\\'); + if (FileName == nullptr) { + FileName = strrchr(FilePath, '/'); + } + if (FileName == nullptr) { + FileName = FilePath; // There was no path in FilePath + } else { + FileName += 1; + } + + QuicEventInitialize(&SendReady, FALSE, FALSE); + + HQUIC Registration = nullptr; + HQUIC Session = nullptr; + HQUIC Connection = nullptr; + HQUIC Stream = nullptr; + + EXIT_ON_FAILURE(MsQuicOpen(&MsQuic)); + const QUIC_REGISTRATION_CONFIG RegConfig = { "post", QUIC_EXECUTION_PROFILE_LOW_LATENCY }; + EXIT_ON_FAILURE(MsQuic->RegistrationOpen(&RegConfig, &Registration)); + EXIT_ON_FAILURE(MsQuic->SessionOpen(Registration, ALPNs, ARRAYSIZE(ALPNs), nullptr, &Session)); + EXIT_ON_FAILURE(MsQuic->ConnectionOpen(Session, ConnectionHandler, nullptr, &Connection)); + EXIT_ON_FAILURE(MsQuic->SetParam(Connection, QUIC_PARAM_LEVEL_CONNECTION, QUIC_PARAM_CONN_CERT_VALIDATION_FLAGS, sizeof(CertificateValidationFlags), &CertificateValidationFlags)); + EXIT_ON_FAILURE(MsQuic->StreamOpen(Connection, QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL, StreamHandler, nullptr, &Stream)); + EXIT_ON_FAILURE(MsQuic->StreamStart(Stream, QUIC_STREAM_START_FLAG_ASYNC)); + EXIT_ON_FAILURE(MsQuic->ConnectionStart(Connection, AF_UNSPEC, ServerName, Port)); + + printf("POST '%s' to %s:%hu\n", FileName, ServerName, Port); + + uint64_t TotalBytesSent = 0; + uint64_t TimeStart = QuicTimeUs64(); + + uint8_t Buffer[IO_SIZE]; + QUIC_BUFFER SendBuffer = { 0, Buffer }; + SendBuffer.Length = snprintf((char*)Buffer, sizeof(Buffer), POST_HEADER_FORMAT, FileName); + + bool EndOfFile = false; + do { + SendBuffer.Length += (uint32_t) + fread( + SendBuffer.Buffer + SendBuffer.Length, + 1, + sizeof(Buffer) - SendBuffer.Length, + File); + EndOfFile = SendBuffer.Length != sizeof(Buffer); + EXIT_ON_FAILURE(MsQuic->StreamSend(Stream, &SendBuffer, 1, EndOfFile ? QUIC_SEND_FLAG_FIN : QUIC_SEND_FLAG_NONE, nullptr)); + QuicEventWaitForever(SendReady); + TotalBytesSent += SendBuffer.Length; + SendBuffer.Length = 0; + } while (!TransferCanceled && !EndOfFile); + + MsQuic->SessionClose(Session); + MsQuic->RegistrationClose(Registration); + MsQuicClose(MsQuic); + + uint64_t TimeEnd = QuicTimeUs64(); + uint64_t ElapsedUs = QuicTimeDiff64(TimeStart, TimeEnd); + uint64_t SendRateKbps = (TotalBytesSent * 1000 * 8) / ElapsedUs; + + printf("%llu bytes sent in %llu.%03llu ms ", TotalBytesSent, ElapsedUs / 1000, ElapsedUs % 1000); + if (SendRateKbps > 1000) { + printf("(%llu.%03llu mbps)\n", SendRateKbps / 1000, SendRateKbps % 1000); + } else { + printf("(%llu kbps)\n", SendRateKbps); + } + + QuicEventUninitialize(SendReady); + fclose(File); + + QuicPlatformUninitialize(); + QuicPlatformSystemUnload(); + + return 0; +}