зеркало из https://github.com/microsoft/msquic.git
Add basic upload support to interop server (#326)
Co-authored-by: Nick Banks <nibanks@microsoft.com>
This commit is contained in:
Родитель
23de1390cd
Коммит
df5b9f6b7d
|
@ -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)
|
||||
|
|
|
@ -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:<addr or *> -root:<path>"
|
||||
" [-thumbprint:<cert_thumbprint>] [-name:<cert_name>]"
|
||||
" [-file:<cert_filepath> AND -key:<cert_key_filepath>]"
|
||||
" [-port:<####> (def:%u)] [-retry:<0/1> (def:%u)]\n\n",
|
||||
" [-port:<####> (def:%u)] [-retry:<0/1> (def:%u)]"
|
||||
" [-upload:<path>]\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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
|
@ -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 <msquichelper.h>
|
||||
|
||||
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:<name>] [-ip:<ip>] [-port:<number>] -file:<path>\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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче