diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 7be08a476..3337580a5 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "common/archives.h" #include "common/assert.h" #include "common/scope_exit.h" @@ -34,36 +35,78 @@ enum { InvalidRequestMethod = 32, HeaderNotFound = 40, BufferTooSmall = 43, + + /// This error is returned in multiple situations: when trying to add Post data that is + /// incompatible with the one that is used in the session, or when trying to use chunked + /// requests with Post data already set + IncompatibleAddPostData = 50, + + InvalidPostDataEncoding = 53, + IncompatibleSendPostData = 54, + WrongCertID = 57, + CertAlreadySet = 61, ContextNotFound = 100, + Timeout = 105, /// This error is returned in multiple situations: when trying to initialize an /// already-initialized session, or when using the wrong context handle in a context-bound /// session SessionStateError = 102, + + WrongCertHandle = 201, TooManyClientCerts = 203, NotImplemented = 1012, }; } -const Result ERROR_STATE_ERROR = // 0xD8A0A066 +constexpr Result ErrorStateError = // 0xD8A0A066 Result(ErrCodes::SessionStateError, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent); -const Result ERROR_NOT_IMPLEMENTED = // 0xD960A3F4 +constexpr Result ErrorNotImplemented = // 0xD960A3F4 Result(ErrCodes::NotImplemented, ErrorModule::HTTP, ErrorSummary::Internal, ErrorLevel::Permanent); -const Result ERROR_TOO_MANY_CLIENT_CERTS = // 0xD8A0A0CB +constexpr Result ErrorTooManyClientCerts = // 0xD8A0A0CB Result(ErrCodes::TooManyClientCerts, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent); -const Result ERROR_HEADER_NOT_FOUND = Result(ErrCodes::HeaderNotFound, ErrorModule::HTTP, - ErrorSummary::InvalidState, ErrorLevel::Permanent); -const Result ERROR_BUFFER_SMALL = Result(ErrCodes::BufferTooSmall, ErrorModule::HTTP, - ErrorSummary::WouldBlock, ErrorLevel::Permanent); -const Result ERROR_WRONG_CERT_ID = // 0xD8E0B839 - Result(57, ErrorModule::SSL, ErrorSummary::InvalidArgument, ErrorLevel::Permanent); -const Result ERROR_WRONG_CERT_HANDLE = // 0xD8A0A0C9 - Result(201, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent); -const Result ERROR_CERT_ALREADY_SET = // 0xD8A0A03D - Result(61, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent); +constexpr Result ErrorHeaderNotFound = // 0xD8A0A028 + Result(ErrCodes::HeaderNotFound, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorBufferSmall = // 0xD840A02B + Result(ErrCodes::BufferTooSmall, ErrorModule::HTTP, ErrorSummary::WouldBlock, + ErrorLevel::Permanent); +constexpr Result ErrorWrongCertID = // 0xD8E0B839 + Result(ErrCodes::WrongCertID, ErrorModule::SSL, ErrorSummary::InvalidArgument, + ErrorLevel::Permanent); +constexpr Result ErrorWrongCertHandle = // 0xD8A0A0C9 + Result(ErrCodes::WrongCertHandle, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorCertAlreadySet = // 0xD8A0A03D + Result(ErrCodes::CertAlreadySet, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorIncompatibleAddPostData = // 0xD8A0A032 + Result(ErrCodes::IncompatibleAddPostData, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorContextNotFound = // 0xD8A0A064 + Result(ErrCodes::ContextNotFound, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorTimeout = // 0xD820A069 + Result(ErrCodes::Timeout, ErrorModule::HTTP, ErrorSummary::NothingHappened, + ErrorLevel::Permanent); +constexpr Result ErrorTooManyContexts = // 0xD8A0A01A + Result(ErrCodes::TooManyContexts, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorInvalidRequestMethod = // 0xD8A0A020 + Result(ErrCodes::InvalidRequestMethod, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorInvalidRequestState = // 0xD8A0A016 + Result(ErrCodes::InvalidRequestState, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorInvalidPostDataEncoding = // 0xD8A0A035 + Result(ErrCodes::InvalidPostDataEncoding, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); +constexpr Result ErrorIncompatibleSendPostData = // 0xD8A0A036 + Result(ErrCodes::IncompatibleSendPostData, ErrorModule::HTTP, ErrorSummary::InvalidState, + ErrorLevel::Permanent); // Splits URL into its components. Example: https://citra-emu.org:443/index.html // is_https: true; host: citra-emu.org; port: 443; path: /index.html @@ -130,8 +173,36 @@ static std::size_t WriteHeaders(httplib::Stream& stream, return write_len; } -static std::size_t HandleHeaderWrite(std::vector& pending_headers, - httplib::Stream& strm, httplib::Headers& httplib_headers) { +static void SerializeChunkedAsciiPostData(httplib::DataSink& sink, const Context::Params& params) { + std::string query; + + for (auto it = params.begin(); it != params.end(); ++it) { + if (it != params.begin()) { + sink.os << "&"; + } + + query = + fmt::format("{}={}", it->first, httplib::detail::encode_query_param(it->second.value)); + boost::replace_all(query, "*", "%2A"); + sink.os << query; + } +} + +static void SerializeChunkedMultipartPostData(httplib::DataSink& sink, + const Context::Params& params, + const std::string& boundary) { + for (const auto& param : params) { + const auto item = param.second.ToMultipartForm(); + std::string body = httplib::detail::serialize_multipart_formdata_item_begin(item, boundary); + body += item.content + httplib::detail::serialize_multipart_formdata_item_end(); + sink.os << body; + } + + sink.os << httplib::detail::serialize_multipart_formdata_finish(boundary); +} + +std::size_t Context::HandleHeaderWrite(std::vector& pending_headers, + httplib::Stream& strm, httplib::Headers& httplib_headers) { std::vector final_headers; std::vector::iterator it_pending_headers; httplib::Headers::iterator it_httplib_headers; @@ -164,24 +235,51 @@ static std::size_t HandleHeaderWrite(std::vector& pendin // Fourth: Content-Length it_pending_headers = find_pending_header("Content-Length"); - if (it_pending_headers != pending_headers.end()) { - final_headers.push_back( - Context::RequestHeader(it_pending_headers->name, it_pending_headers->value)); - pending_headers.erase(it_pending_headers); - } else { - it_httplib_headers = httplib_headers.find("Content-Length"); - if (it_httplib_headers != httplib_headers.end()) { - final_headers.push_back( - Context::RequestHeader(it_httplib_headers->first, it_httplib_headers->second)); + if (it_pending_headers == pending_headers.end()) { + if ((method == RequestMethod::Post || method == RequestMethod::Put) && !chunked_request) { + it_httplib_headers = httplib_headers.find("Content-Length"); + if (it_httplib_headers != httplib_headers.end()) { + final_headers.push_back( + Context::RequestHeader(it_httplib_headers->first, it_httplib_headers->second)); + } } } + // Fifth: Transfer-Encoding + if (chunked_request) { + final_headers.push_back(Context::RequestHeader("Transfer-Encoding", "chunked")); + } + return WriteHeaders(strm, final_headers); }; +void Context::ParseAsciiPostData() { + httplib::Params ascii_form; + for (auto param : post_data) { + ascii_form.emplace(param.first, param.second.value); + } + + post_data_raw = httplib::detail::params_to_query_str(ascii_form); + boost::replace_all(post_data_raw, "*", "%2A"); +} + +std::string Context::ParseMultipartFormData() { + httplib::MultipartFormDataItems multipart_form; + for (auto param : post_data) { + multipart_form.push_back(param.second.ToMultipartForm()); + } + + multipart_boundary = httplib::detail::make_multipart_data_boundary(); + post_data_raw = + httplib::detail::serialize_multipart_formdata(multipart_form, multipart_boundary); + return httplib::detail::serialize_multipart_formdata_get_content_type(multipart_boundary); +} + void Context::MakeRequest() { ASSERT(state == RequestState::NotStarted); + state = RequestState::ConnectingToServer; + static const std::unordered_map request_method_strings{ {RequestMethod::Get, "GET"}, {RequestMethod::Post, "POST"}, {RequestMethod::Head, "HEAD"}, {RequestMethod::Put, "PUT"}, @@ -207,18 +305,61 @@ void Context::MakeRequest() { pending_headers.push_back(header); } - if (!post_data.empty()) { - pending_headers.push_back( - Context::RequestHeader("Content-Type", "application/x-www-form-urlencoded")); - request.body = httplib::detail::params_to_query_str(post_data); - boost::replace_all(request.body, "*", "%2A"); + httplib::Params ascii_form; + httplib::MultipartFormDataItems multipart_form; + if ((method == RequestMethod::Post || method == RequestMethod::Put) && !chunked_request) { + switch (post_data_encoding) { + case PostDataEncoding::AsciiForm: + ParseAsciiPostData(); + pending_headers.push_back( + Context::RequestHeader("Content-Type", "application/x-www-form-urlencoded")); + break; + case PostDataEncoding::MultipartForm: + pending_headers.push_back( + Context::RequestHeader("Content-Type", ParseMultipartFormData())); + break; + case PostDataEncoding::Auto: + if (!post_data.empty()) { + if (force_multipart) { + pending_headers.push_back( + Context::RequestHeader("Content-Type", ParseMultipartFormData())); + } else { + pending_headers.push_back(Context::RequestHeader( + "Content-Type", "application/x-www-form-urlencoded")); + ParseAsciiPostData(); + } + } + break; + } } - if (!post_data_raw.empty()) { - request.body = post_data_raw; - } + // httplib doesn't expose setting the content provider for the request when not using the usual + // send methods like Client::Post or Client::Put, so we have to set the internal fields manually + if (!chunked_request) { + request.content_length_ = post_data_raw.size(); + request.content_provider_ = [this](size_t offset, size_t length, httplib::DataSink& sink) { + return ContentProvider(offset, length, sink); + }; + } else { + if (post_data_type == PostDataType::MultipartForm) { + multipart_boundary = httplib::detail::make_multipart_data_boundary(); + pending_headers.push_back(Context::RequestHeader( + "Content-Type", httplib::detail::serialize_multipart_formdata_get_content_type( + multipart_boundary))); + } - state = RequestState::InProgress; + if (post_data_type == PostDataType::Raw && chunked_content_length > 0) { + pending_headers.push_back(Context::RequestHeader( + "Content-Length", fmt::format("{}", chunked_content_length))); + } + + request.content_length_ = 0; + request.content_provider_ = + httplib::detail::ContentProviderAdapter([this](size_t offset, httplib::DataSink& sink) { + return ChunkedContentProvider(offset, sink); + }); + request.is_chunked_content_provider_ = true; + } if (url_info.is_https) { MakeRequestSSL(request, url_info, pending_headers); @@ -234,7 +375,7 @@ void Context::MakeRequestNonSSL(httplib::Request& request, const URLInfo& url_in std::make_unique(url_info.host, url_info.port); client->set_header_writer( - [&pending_headers](httplib::Stream& strm, httplib::Headers& httplib_headers) { + [this, &pending_headers](httplib::Stream& strm, httplib::Headers& httplib_headers) { return HandleHeaderWrite(pending_headers, strm, httplib_headers); }); @@ -243,7 +384,6 @@ void Context::MakeRequestNonSSL(httplib::Request& request, const URLInfo& url_in state = RequestState::TimedOut; } else { LOG_DEBUG(Service_HTTP, "Request successful"); - // TODO(B3N30): Verify this state on HW state = RequestState::ReadyToDownloadContent; } } @@ -293,7 +433,7 @@ void Context::MakeRequestSSL(httplib::Request& request, const URLInfo& url_info, client->enable_server_certificate_verification(false); client->set_header_writer( - [&pending_headers](httplib::Stream& strm, httplib::Headers& httplib_headers) { + [this, &pending_headers](httplib::Stream& strm, httplib::Headers& httplib_headers) { return HandleHeaderWrite(pending_headers, strm, httplib_headers); }); @@ -302,11 +442,48 @@ void Context::MakeRequestSSL(httplib::Request& request, const URLInfo& url_info, state = RequestState::TimedOut; } else { LOG_DEBUG(Service_HTTP, "Request successful"); - // TODO(B3N30): Verify this state on HW state = RequestState::ReadyToDownloadContent; } } +bool Context::ContentProvider(size_t offset, size_t length, httplib::DataSink& sink) { + state = RequestState::SendingRequest; + + if (!post_data_raw.empty()) { + sink.write(post_data_raw.data() + offset, length); + } + + // This state is set after sending the request, even if it hasn't received a response yet + state = RequestState::ReceivingResponse; + return true; +} + +bool Context::ChunkedContentProvider(size_t offset, httplib::DataSink& sink) { + state = RequestState::SendingRequest; + + finish_post_data.Wait(); + + switch (post_data_type) { + case PostDataType::AsciiForm: + SerializeChunkedAsciiPostData(sink, post_data); + break; + case PostDataType::MultipartForm: + SerializeChunkedMultipartPostData(sink, post_data, multipart_boundary); + break; + // Write the data values + case PostDataType::Raw: + for (const auto& data : post_data) { + sink.os << data.second.value; + } + break; + } + + sink.done(); + // This state is set after sending the request, even if it hasn't received a response yet + state = RequestState::ReceivingResponse; + return true; +} + void HTTP_C::Initialize(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 shmem_size = rp.Pop(); @@ -324,7 +501,7 @@ void HTTP_C::Initialize(Kernel::HLERequestContext& ctx) { if (session_data->initialized) { LOG_ERROR(Service_HTTP, "Tried to initialize an already initialized session"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return; } @@ -350,7 +527,7 @@ void HTTP_C::InitializeConnectionSession(Kernel::HLERequestContext& ctx) { if (session_data->initialized) { LOG_ERROR(Service_HTTP, "Tried to initialize an already initialized session"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return; } @@ -358,8 +535,7 @@ void HTTP_C::InitializeConnectionSession(Kernel::HLERequestContext& ctx) { auto itr = contexts.find(context_handle); if (itr == contexts.end()) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrCodes::ContextNotFound, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorContextNotFound); return; } @@ -388,7 +564,7 @@ void HTTP_C::BeginRequest(Kernel::HLERequestContext& ctx) { if (http_context.uses_default_client_cert && !http_context.clcert_data->init) { LOG_ERROR(Service_HTTP, "Failed to begin HTTP request: client cert not found."); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return; } @@ -399,9 +575,12 @@ void HTTP_C::BeginRequest(Kernel::HLERequestContext& ctx) { // Then there are 3? worker threads that pop the requests from the queue and send them // For now make every request async in it's own thread. - http_context.request_future = - std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); - http_context.current_copied_data = 0; + // This always returns success, but the request is only performed when it hasn't started + if (http_context.state == RequestState::NotStarted) { + http_context.request_future = + std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); + http_context.current_copied_data = 0; + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); @@ -423,7 +602,7 @@ void HTTP_C::BeginRequestAsync(Kernel::HLERequestContext& ctx) { if (http_context.uses_default_client_cert && !http_context.clcert_data->init) { LOG_ERROR(Service_HTTP, "Failed to begin HTTP request: client cert not found."); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return; } @@ -434,9 +613,12 @@ void HTTP_C::BeginRequestAsync(Kernel::HLERequestContext& ctx) { // Then there are 3? worker threads that pop the requests from the queue and send them // For now make every request async in it's own thread. - http_context.request_future = - std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); - http_context.current_copied_data = 0; + // This always returns success, but the request is only performed when it hasn't started + if (http_context.state == RequestState::NotStarted) { + http_context.request_future = + std::async(std::launch::async, &Context::MakeRequest, std::ref(http_context)); + http_context.current_copied_data = 0; + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); @@ -489,9 +671,7 @@ void HTTP_C::ReceiveDataImpl(Kernel::HLERequestContext& ctx, bool timeout) { const auto wait_res = http_context.request_future.wait_for( std::chrono::nanoseconds(async_data->timeout_nanos)); if (wait_res == std::future_status::timeout) { - async_data->async_res = - Result(105, ErrorModule::HTTP, ErrorSummary::NothingHappened, - ErrorLevel::Permanent); + async_data->async_res = ErrorTimeout; } } else { http_context.request_future.wait(); @@ -522,7 +702,7 @@ void HTTP_C::ReceiveDataImpl(Kernel::HLERequestContext& ctx, bool timeout) { http_context.current_copied_data, 0, async_data->buffer_size); http_context.current_copied_data += async_data->buffer_size; - rb.Push(ERROR_BUFFER_SMALL); + rb.Push(ErrorBufferSmall); } LOG_DEBUG(Service_HTTP, "Receive: buffer_size= {}, total_copied={}, total_body={}", async_data->buffer_size, http_context.current_copied_data, @@ -562,8 +742,7 @@ void HTTP_C::CreateContext(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "Command called with a bound context"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrorDescription::NotImplemented, ErrorModule::HTTP, ErrorSummary::Internal, - ErrorLevel::Permanent)); + rb.Push(ErrorNotImplemented); rb.PushMappedBuffer(buffer); return; } @@ -574,8 +753,7 @@ void HTTP_C::CreateContext(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "Tried to open too many HTTP contexts"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrCodes::TooManyContexts, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorTooManyContexts); rb.PushMappedBuffer(buffer); return; } @@ -584,8 +762,7 @@ void HTTP_C::CreateContext(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "invalid request method={}", method); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrCodes::InvalidRequestMethod, ErrorModule::HTTP, - ErrorSummary::InvalidState, ErrorLevel::Permanent)); + rb.Push(ErrorInvalidRequestMethod); rb.PushMappedBuffer(buffer); return; } @@ -659,6 +836,24 @@ void HTTP_C::CancelConnection(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void HTTP_C::GetRequestState(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + + const auto* session_data = EnsureSessionInitialized(ctx, rp); + if (!session_data) { + return; + } + + LOG_DEBUG(Service_HTTP, "called, context_handle={}", context_handle); + + Context& http_context = GetContext(context_handle); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.PushEnum(http_context.state); +} + void HTTP_C::AddRequestHeader(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 context_handle = rp.Pop(); @@ -687,8 +882,7 @@ void HTTP_C::AddRequestHeader(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "Tried to add a request header on a context that has already been started."); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrCodes::InvalidRequestState, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorInvalidRequestState); rb.PushMappedBuffer(value_buffer); return; } @@ -713,7 +907,7 @@ void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) { // Copy the value_buffer into a string without the \0 at the end std::string value(value_size - 1, '\0'); - value_buffer.Read(&value[0], 0, value_size - 1); + value_buffer.Read(value.data(), 0, value_size - 1); LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value, context_handle); @@ -726,15 +920,89 @@ void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) { if (http_context.state != RequestState::NotStarted) { LOG_ERROR(Service_HTTP, - "Tried to add post data on a context that has already been started."); + "Tried to add Post data on a context that has already been started"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrCodes::InvalidRequestState, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorInvalidRequestState); rb.PushMappedBuffer(value_buffer); return; } - http_context.post_data.emplace(name, value); + if (!http_context.post_data_raw.empty()) { + LOG_ERROR(Service_HTTP, "Cannot add ASCII Post data to context with raw Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); + rb.PushMappedBuffer(value_buffer); + return; + } + + if (http_context.chunked_request) { + LOG_ERROR(Service_HTTP, "Cannot add ASCII Post data to context in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); + rb.PushMappedBuffer(value_buffer); + return; + } + + Context::Param param_value(name, value); + http_context.post_data.emplace(name, param_value); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(value_buffer); +} + +void HTTP_C::AddPostDataBinary(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + [[maybe_unused]] const u32 name_size = rp.Pop(); + const u32 value_size = rp.Pop(); + const std::vector name_buffer = rp.PopStaticBuffer(); + Kernel::MappedBuffer& value_buffer = rp.PopMappedBuffer(); + + // Copy the name_buffer into a string without the \0 at the end + const std::string name(name_buffer.begin(), name_buffer.end() - 1); + + // Copy the value_buffer into a vector + std::vector value(value_size); + value_buffer.Read(value.data(), 0, value_size); + + LOG_DEBUG(Service_HTTP, "called, name={}, value_size={}, context_handle={}", name, value_size, + context_handle); + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to add Post data on a context that has already been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(value_buffer); + return; + } + + if (!http_context.post_data_raw.empty()) { + LOG_ERROR(Service_HTTP, "Cannot add Binary Post data to context with raw Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); + rb.PushMappedBuffer(value_buffer); + return; + } + + if (http_context.chunked_request) { + LOG_ERROR(Service_HTTP, "Cannot add Binary Post data to context in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); + rb.PushMappedBuffer(value_buffer); + return; + } + + Context::Param param_value(name, value); + http_context.post_data.emplace(name, param_value); + http_context.force_multipart = true; IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); @@ -758,10 +1026,26 @@ void HTTP_C::AddPostDataRaw(Kernel::HLERequestContext& ctx) { if (http_context.state != RequestState::NotStarted) { LOG_ERROR(Service_HTTP, - "Tried to add post data on a context that has already been started."); + "Tried to add Post data on a context that has already been started"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(Result(ErrCodes::InvalidRequestState, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(buffer); + return; + } + + if (!http_context.post_data.empty()) { + LOG_ERROR(Service_HTTP, + "Cannot add raw Post data to context with ASCII or Binary Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); + rb.PushMappedBuffer(buffer); + return; + } + + if (http_context.chunked_request) { + LOG_ERROR(Service_HTTP, "Cannot add raw Post data to context in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleAddPostData); rb.PushMappedBuffer(buffer); return; } @@ -774,21 +1058,353 @@ void HTTP_C::AddPostDataRaw(Kernel::HLERequestContext& ctx) { rb.PushMappedBuffer(buffer); } +void HTTP_C::SetPostDataType(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + const PostDataType type = rp.PopEnum(); + + LOG_DEBUG(Service_HTTP, "called, context_handle={}, type={}", context_handle, type); + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to set chunked mode on a context that has already been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + if (!http_context.post_data.empty() || !http_context.post_data_raw.empty()) { + LOG_ERROR(Service_HTTP, "Tried to set chunked mode on a context that has Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorIncompatibleSendPostData); + return; + } + + switch (type) { + case PostDataType::AsciiForm: + case PostDataType::MultipartForm: + case PostDataType::Raw: + http_context.post_data_type = type; + break; + // Use ASCII form by default + default: + http_context.post_data_type = PostDataType::AsciiForm; + break; + } + + http_context.chunked_request = true; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void HTTP_C::SendPostDataAscii(Kernel::HLERequestContext& ctx) { + SendPostDataAsciiImpl(ctx, false); +} + +void HTTP_C::SendPostDataAsciiTimeout(Kernel::HLERequestContext& ctx) { + SendPostDataAsciiImpl(ctx, true); +} + +void HTTP_C::SendPostDataAsciiImpl(Kernel::HLERequestContext& ctx, bool timeout) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + [[maybe_unused]] const u32 name_size = rp.Pop(); + const u32 value_size = rp.Pop(); + // TODO(DaniElectra): The original module waits until a connection with the server is made + const u64 timeout_nanos = timeout ? rp.Pop() : 0; + const std::vector name_buffer = rp.PopStaticBuffer(); + Kernel::MappedBuffer& value_buffer = rp.PopMappedBuffer(); + + // Copy the name_buffer into a string without the \0 at the end + const std::string name(name_buffer.begin(), name_buffer.end() - 1); + + // Copy the value_buffer into a string without the \0 at the end + std::string value(value_size - 1, '\0'); + value_buffer.Read(value.data(), 0, value_size - 1); + + if (timeout) { + LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}, timeout={}", name, + value, context_handle, timeout_nanos); + } else { + LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value, + context_handle); + } + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state == RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, "Tried to send Post data on a context that has not been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(value_buffer); + return; + } + + if (!http_context.chunked_request) { + LOG_ERROR(Service_HTTP, + "Cannot send ASCII Post data to context not in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(value_buffer); + return; + } + + Context::Param param_value(name, value); + http_context.post_data.emplace(name, param_value); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(value_buffer); +} + +void HTTP_C::SendPostDataBinary(Kernel::HLERequestContext& ctx) { + SendPostDataBinaryImpl(ctx, false); +} + +void HTTP_C::SendPostDataBinaryTimeout(Kernel::HLERequestContext& ctx) { + SendPostDataBinaryImpl(ctx, true); +} + +void HTTP_C::SendPostDataBinaryImpl(Kernel::HLERequestContext& ctx, bool timeout) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + [[maybe_unused]] const u32 name_size = rp.Pop(); + const u32 value_size = rp.Pop(); + // TODO(DaniElectra): The original module waits until a connection with the server is made + const u64 timeout_nanos = timeout ? rp.Pop() : 0; + const std::vector name_buffer = rp.PopStaticBuffer(); + Kernel::MappedBuffer& value_buffer = rp.PopMappedBuffer(); + + // Copy the name_buffer into a string without the \0 at the end + const std::string name(name_buffer.begin(), name_buffer.end() - 1); + + // Copy the value_buffer into a vector + std::vector value(value_size); + value_buffer.Read(value.data(), 0, value_size); + + if (timeout) { + LOG_DEBUG(Service_HTTP, "called, name={}, value_size={}, context_handle={}, timeout={}", + name, value_size, context_handle, timeout_nanos); + } else { + LOG_DEBUG(Service_HTTP, "called, name={}, value_size={}, context_handle={}", name, + value_size, context_handle); + } + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state == RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, "Tried to add Post data on a context that has not been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(value_buffer); + return; + } + + if (!http_context.chunked_request) { + LOG_ERROR(Service_HTTP, + "Cannot send Binary Post data to context not in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(value_buffer); + return; + } + + Context::Param param_value(name, value); + http_context.post_data.emplace(name, param_value); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(value_buffer); +} + +void HTTP_C::SendPostDataRaw(Kernel::HLERequestContext& ctx) { + SendPostDataRawImpl(ctx, false); +} + +void HTTP_C::SendPostDataRawTimeout(Kernel::HLERequestContext& ctx) { + SendPostDataRawImpl(ctx, true); +} + +void HTTP_C::SendPostDataRawImpl(Kernel::HLERequestContext& ctx, bool timeout) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + const u32 post_data_len = rp.Pop(); + // TODO(DaniElectra): The original module waits until a connection with the server is made + const u64 timeout_nanos = timeout ? rp.Pop() : 0; + auto buffer = rp.PopMappedBuffer(); + + if (timeout) { + LOG_DEBUG(Service_HTTP, "called, context_handle={}, post_data_len={}, timeout={}", + context_handle, post_data_len, timeout_nanos); + } else { + LOG_DEBUG(Service_HTTP, "called, context_handle={}, post_data_len={}", context_handle, + post_data_len); + } + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to add Post data on a context that has already been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(buffer); + return; + } + + if (!http_context.chunked_request) { + LOG_ERROR(Service_HTTP, "Cannot send raw Post data to context not in chunked request mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorInvalidRequestState); + rb.PushMappedBuffer(buffer); + return; + } + + if (http_context.post_data_type != PostDataType::Raw) { + LOG_ERROR(Service_HTTP, "Cannot send raw Post data to context not in raw mode"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ErrorIncompatibleSendPostData); + rb.PushMappedBuffer(buffer); + return; + } + + std::vector value(buffer.GetSize()); + buffer.Read(value.data(), 0, value.size()); + + // Workaround for sending the raw data in combination of other data in chunked requests + Context::Param raw_param(value); + std::string value_string(value.begin(), value.end()); + http_context.post_data.emplace(value_string, raw_param); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(buffer); +} + +void HTTP_C::SetPostDataEncoding(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + const PostDataEncoding encoding = rp.PopEnum(); + + LOG_DEBUG(Service_HTTP, "called, context_handle={}, encoding={}", context_handle, encoding); + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to set Post encoding on a context that has already been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + switch (encoding) { + case PostDataEncoding::Auto: + http_context.post_data_encoding = encoding; + break; + case PostDataEncoding::AsciiForm: + case PostDataEncoding::MultipartForm: + if (!http_context.post_data_raw.empty()) { + LOG_ERROR(Service_HTTP, "Cannot set Post data encoding to context with raw Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorIncompatibleAddPostData); + return; + } + + http_context.post_data_encoding = encoding; + break; + default: + LOG_ERROR(Service_HTTP, "Invalid Post data encoding: {}", encoding); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidPostDataEncoding); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void HTTP_C::NotifyFinishSendPostData(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + + LOG_DEBUG(Service_HTTP, "called, context_handle={}", context_handle); + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state == RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to notfy finish Post on a context that has not been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + http_context.finish_post_data.Set(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + void HTTP_C::GetResponseHeader(Kernel::HLERequestContext& ctx) { + GetResponseHeaderImpl(ctx, false); +} + +void HTTP_C::GetResponseHeaderTimeout(Kernel::HLERequestContext& ctx) { + GetResponseHeaderImpl(ctx, true); +} + +void HTTP_C::GetResponseHeaderImpl(Kernel::HLERequestContext& ctx, bool timeout) { IPC::RequestParser rp(ctx); struct AsyncData { + // Input u32 context_handle; u32 name_len; u32 value_max_len; + bool timeout; + u64 timeout_nanos; std::span header_name; Kernel::MappedBuffer* value_buffer; + // Output + Result async_res = ResultSuccess; }; std::shared_ptr async_data = std::make_shared(); + async_data->timeout = timeout; async_data->context_handle = rp.Pop(); async_data->name_len = rp.Pop(); async_data->value_max_len = rp.Pop(); + if (timeout) { + async_data->timeout_nanos = rp.Pop(); + } async_data->header_name = rp.PopStaticBuffer(); async_data->value_buffer = &rp.PopMappedBuffer(); @@ -799,10 +1415,27 @@ void HTTP_C::GetResponseHeader(Kernel::HLERequestContext& ctx) { ctx.RunAsync( [this, async_data](Kernel::HLERequestContext& ctx) { Context& http_context = GetContext(async_data->context_handle); - http_context.request_future.wait(); + + if (async_data->timeout) { + const auto wait_res = http_context.request_future.wait_for( + std::chrono::nanoseconds(async_data->timeout_nanos)); + if (wait_res == std::future_status::timeout) { + async_data->async_res = ErrorTimeout; + } + } else { + http_context.request_future.wait(); + } + return 0; }, [this, async_data](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, static_cast(ctx.CommandHeader().command_id.Value()), 2, + 2); + if (async_data->async_res != ResultSuccess) { + rb.Push(async_data->async_res); + return; + } + std::string header_name_str( reinterpret_cast(async_data->header_name.data()), async_data->name_len); @@ -813,8 +1446,13 @@ void HTTP_C::GetResponseHeader(Kernel::HLERequestContext& ctx) { auto& headers = http_context.response.headers; u32 copied_size = 0; - LOG_DEBUG(Service_HTTP, "header={}, max_len={}", header_name_str, - async_data->value_buffer->GetSize()); + if (async_data->timeout) { + LOG_DEBUG(Service_HTTP, "header={}, max_len={}, timeout={}", header_name_str, + async_data->value_buffer->GetSize(), async_data->timeout_nanos); + } else { + LOG_DEBUG(Service_HTTP, "header={}, max_len={}", header_name_str, + async_data->value_buffer->GetSize()); + } auto header = headers.find(header_name_str); if (header != headers.end()) { @@ -827,15 +1465,12 @@ void HTTP_C::GetResponseHeader(Kernel::HLERequestContext& ctx) { async_data->value_buffer->Write(header_value.data(), 0, header_value.size()); } else { LOG_DEBUG(Service_HTTP, "header={} not found", header_name_str); - IPC::RequestBuilder rb( - ctx, static_cast(ctx.CommandHeader().command_id.Value()), 1, 2); - rb.Push(ERROR_HEADER_NOT_FOUND); + rb.Push(ErrorHeaderNotFound); + rb.Push(0); rb.PushMappedBuffer(*async_data->value_buffer); return; } - IPC::RequestBuilder rb(ctx, static_cast(ctx.CommandHeader().command_id.Value()), 2, - 2); rb.Push(ResultSuccess); rb.Push(copied_size); rb.PushMappedBuffer(*async_data->value_buffer); @@ -886,9 +1521,7 @@ void HTTP_C::GetResponseStatusCodeImpl(Kernel::HLERequestContext& ctx, bool time std::chrono::nanoseconds(async_data->timeout_nanos)); if (wait_res == std::future_status::timeout) { LOG_DEBUG(Service_HTTP, "Status code: {}", "timeout"); - async_data->async_res = - Result(105, ErrorModule::HTTP, ErrorSummary::NothingHappened, - ErrorLevel::Permanent); + async_data->async_res = ErrorTimeout; } } else { http_context.request_future.wait(); @@ -955,7 +1588,7 @@ void HTTP_C::SetDefaultClientCert(Kernel::HLERequestContext& ctx) { if (client_cert_id != ClientCertID::Default) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_WRONG_CERT_ID); + rb.Push(ErrorWrongCertID); return; } @@ -984,7 +1617,7 @@ void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) { if (cert_context_itr == client_certs.end()) { LOG_ERROR(Service_HTTP, "called with wrong client_cert_handle {}", client_cert_handle); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_WRONG_CERT_HANDLE); + rb.Push(ErrorWrongCertHandle); return; } @@ -992,7 +1625,7 @@ void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "Tried to set a client cert to a context that already has a client cert"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_CERT_ALREADY_SET); + rb.Push(ErrorCertAlreadySet); return; } @@ -1000,8 +1633,7 @@ void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) { LOG_ERROR(Service_HTTP, "Tried to set a client cert on a context that has already been started."); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrCodes::InvalidRequestState, ErrorModule::HTTP, ErrorSummary::InvalidState, - ErrorLevel::Permanent)); + rb.Push(ErrorInvalidRequestState); return; } @@ -1053,13 +1685,13 @@ void HTTP_C::OpenClientCertContext(Kernel::HLERequestContext& ctx) { if (!session_data->initialized) { LOG_ERROR(Service_HTTP, "Command called without Initialize"); - result = ERROR_STATE_ERROR; + result = ErrorStateError; } else if (session_data->current_http_context) { LOG_ERROR(Service_HTTP, "Command called with a bound context"); - result = ERROR_NOT_IMPLEMENTED; + result = ErrorNotImplemented; } else if (session_data->num_client_certs >= 2) { LOG_ERROR(Service_HTTP, "tried to load more then 2 client certs"); - result = ERROR_TOO_MANY_CLIENT_CERTS; + result = ErrorTooManyClientCerts; } else { client_certs[++client_certs_counter] = std::make_shared(); client_certs[client_certs_counter]->handle = client_certs_counter; @@ -1092,14 +1724,14 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) { if (session_data->current_http_context) { LOG_ERROR(Service_HTTP, "Command called with a bound context"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_NOT_IMPLEMENTED); + rb.Push(ErrorNotImplemented); return; } if (session_data->num_client_certs >= 2) { LOG_ERROR(Service_HTTP, "tried to load more then 2 client certs"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_TOO_MANY_CLIENT_CERTS); + rb.Push(ErrorTooManyClientCerts); return; } @@ -1107,7 +1739,7 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) { if (cert_id != default_cert_id) { LOG_ERROR(Service_HTTP, "called with invalid cert_id {}", cert_id); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_WRONG_CERT_ID); + rb.Push(ErrorWrongCertID); return; } @@ -1188,6 +1820,54 @@ void HTTP_C::SetKeepAlive(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); } +void HTTP_C::SetPostDataTypeSize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + const u32 context_handle = rp.Pop(); + const PostDataType type = rp.PopEnum(); + const u32 size = rp.Pop(); + + LOG_DEBUG(Service_HTTP, "called, context_handle={}, type={}, size={}", context_handle, type, + size); + + if (!PerformStateChecks(ctx, rp, context_handle)) { + return; + } + + Context& http_context = GetContext(context_handle); + + if (http_context.state != RequestState::NotStarted) { + LOG_ERROR(Service_HTTP, + "Tried to set chunked mode on a context that has already been started"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorInvalidRequestState); + return; + } + + if (!http_context.post_data.empty() || !http_context.post_data_raw.empty()) { + LOG_ERROR(Service_HTTP, "Tried to set chunked mode on a context that has Post data"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ErrorIncompatibleSendPostData); + return; + } + + switch (type) { + case PostDataType::AsciiForm: + case PostDataType::MultipartForm: + case PostDataType::Raw: + http_context.post_data_type = type; + break; + default: + http_context.post_data_type = PostDataType::AsciiForm; + break; + } + + http_context.chunked_request = true; + http_context.chunked_content_length = size; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + void HTTP_C::Finalize(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1243,7 +1923,7 @@ SessionData* HTTP_C::EnsureSessionInitialized(Kernel::HLERequestContext& ctx, if (!session_data->initialized) { LOG_ERROR(Service_HTTP, "Tried to make a request on an uninitialized session"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return nullptr; } @@ -1262,8 +1942,7 @@ bool HTTP_C::PerformStateChecks(Kernel::HLERequestContext& ctx, IPC::RequestPars LOG_ERROR(Service_HTTP, "Tried to make a request without a bound context"); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotImplemented, ErrorModule::HTTP, ErrorSummary::Internal, - ErrorLevel::Permanent)); + rb.Push(ErrorNotImplemented); return false; } @@ -1273,7 +1952,7 @@ bool HTTP_C::PerformStateChecks(Kernel::HLERequestContext& ctx, IPC::RequestPars "Tried to make a request on a mismatched session input context={} session context={}", context_handle, *session_data->current_http_context); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ERROR_STATE_ERROR); + rb.Push(ErrorStateError); return false; } @@ -1362,7 +2041,7 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) { {0x0002, &HTTP_C::CreateContext, "CreateContext"}, {0x0003, &HTTP_C::CloseContext, "CloseContext"}, {0x0004, &HTTP_C::CancelConnection, "CancelConnection"}, - {0x0005, nullptr, "GetRequestState"}, + {0x0005, &HTTP_C::GetRequestState, "GetRequestState"}, {0x0006, &HTTP_C::GetDownloadSizeState, "GetDownloadSizeState"}, {0x0007, nullptr, "GetRequestError"}, {0x0008, &HTTP_C::InitializeConnectionSession, "InitializeConnectionSession"}, @@ -1376,19 +2055,19 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) { {0x0010, nullptr, "SetSocketBufferSize"}, {0x0011, &HTTP_C::AddRequestHeader, "AddRequestHeader"}, {0x0012, &HTTP_C::AddPostDataAscii, "AddPostDataAscii"}, - {0x0013, nullptr, "AddPostDataBinary"}, + {0x0013, &HTTP_C::AddPostDataBinary, "AddPostDataBinary"}, {0x0014, &HTTP_C::AddPostDataRaw, "AddPostDataRaw"}, - {0x0015, nullptr, "SetPostDataType"}, - {0x0016, nullptr, "SendPostDataAscii"}, - {0x0017, nullptr, "SendPostDataAsciiTimeout"}, - {0x0018, nullptr, "SendPostDataBinary"}, - {0x0019, nullptr, "SendPostDataBinaryTimeout"}, - {0x001A, nullptr, "SendPostDataRaw"}, - {0x001B, nullptr, "SendPOSTDataRawTimeout"}, - {0x001C, nullptr, "SetPostDataEncoding"}, - {0x001D, nullptr, "NotifyFinishSendPostData"}, + {0x0015, &HTTP_C::SetPostDataType, "SetPostDataType"}, + {0x0016, &HTTP_C::SendPostDataAscii, "SendPostDataAscii"}, + {0x0017, &HTTP_C::SendPostDataAsciiTimeout, "SendPostDataAsciiTimeout"}, + {0x0018, &HTTP_C::SendPostDataBinary, "SendPostDataBinary"}, + {0x0019, &HTTP_C::SendPostDataBinaryTimeout, "SendPostDataBinaryTimeout"}, + {0x001A, &HTTP_C::SendPostDataRaw, "SendPostDataRaw"}, + {0x001B, &HTTP_C::SendPostDataRawTimeout, "SendPostDataRawTimeout"}, + {0x001C, &HTTP_C::SetPostDataEncoding, "SetPostDataEncoding"}, + {0x001D, &HTTP_C::NotifyFinishSendPostData, "NotifyFinishSendPostData"}, {0x001E, &HTTP_C::GetResponseHeader, "GetResponseHeader"}, - {0x001F, nullptr, "GetResponseHeaderTimeout"}, + {0x001F, &HTTP_C::GetResponseHeaderTimeout, "GetResponseHeaderTimeout"}, {0x0020, nullptr, "GetResponseData"}, {0x0021, nullptr, "GetResponseDataTimeout"}, {0x0022, &HTTP_C::GetResponseStatusCode, "GetResponseStatusCode"}, @@ -1413,7 +2092,7 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) { {0x0035, nullptr, "SetDefaultProxy"}, {0x0036, nullptr, "ClearDNSCache"}, {0x0037, &HTTP_C::SetKeepAlive, "SetKeepAlive"}, - {0x0038, nullptr, "SetPostDataTypeSize"}, + {0x0038, &HTTP_C::SetPostDataTypeSize, "SetPostDataTypeSize"}, {0x0039, &HTTP_C::Finalize, "Finalize"}, // clang-format on }; diff --git a/src/core/hle/service/http/http_c.h b/src/core/hle/service/http/http_c.h index 75f3b86b9..7f71aebcd 100644 --- a/src/core/hle/service/http/http_c.h +++ b/src/core/hle/service/http/http_c.h @@ -18,6 +18,7 @@ #include #include #include +#include "common/thread.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/shared_memory.h" #include "core/hle/service/service.h" @@ -48,12 +49,25 @@ constexpr u32 TotalRequestMethods = 8; enum class RequestState : u8 { NotStarted = 0x1, // Request has not started yet. - InProgress = 0x5, // Request in progress, sending request over the network. - ReadyToDownloadContent = 0x7, // Ready to download the content. (needs verification) - ReadyToDownload = 0x8, // Ready to download? + ConnectingToServer = 0x5, // Request in progress, connecting to server. + SendingRequest = 0x6, // Request in progress, sending HTTP request. + ReceivingResponse = 0x7, // Request in progress, receiving HTTP response. + ReadyToDownloadContent = 0x8, // Ready to download the content. TimedOut = 0xA, // Request timed out? }; +enum class PostDataEncoding : u8 { + Auto = 0x0, + AsciiForm = 0x1, + MultipartForm = 0x2, +}; + +enum class PostDataType : u8 { + AsciiForm = 0x0, + MultipartForm = 0x1, + Raw = 0x2, +}; + enum class ClientCertID : u32 { Default = 0x40, // Default client cert }; @@ -197,6 +211,41 @@ public: friend class boost::serialization::access; }; + struct Param { + Param(const std::vector& value) + : name(value.begin(), value.end()), value(value.begin(), value.end()){}; + Param(const std::string& name, const std::string& value) : name(name), value(value){}; + Param(const std::string& name, const std::vector& value) + : name(name), value(value.begin(), value.end()), is_binary(true){}; + std::string name; + std::string value; + bool is_binary = false; + + httplib::MultipartFormData ToMultipartForm() const { + httplib::MultipartFormData form; + form.name = name; + form.content = value; + if (is_binary) { + form.content_type = "application/octet-stream"; + // TODO(DaniElectra): httplib doesn't support setting Content-Transfer-Encoding, + // while the 3DS sets Content-Transfer-Encoding: binary if a binary value is set + } + + return form; + } + + private: + template + void serialize(Archive& ar, const unsigned int) { + ar& name; + ar& value; + ar& is_binary; + } + friend class boost::serialization::access; + }; + + using Params = std::multimap; + Handle handle; u32 session_id; std::string url; @@ -208,8 +257,14 @@ public: u32 socket_buffer_size; std::vector headers; const ClCertAData* clcert_data; - httplib::Params post_data; + Params post_data; std::string post_data_raw; + PostDataEncoding post_data_encoding = PostDataEncoding::Auto; + PostDataType post_data_type; + std::string multipart_boundary; + bool force_multipart = false; + bool chunked_request = false; + u32 chunked_content_length; std::future request_future; std::atomic current_download_size_bytes; @@ -217,12 +272,19 @@ public: std::size_t current_copied_data; bool uses_default_client_cert{}; httplib::Response response; + Common::Event finish_post_data; + void ParseAsciiPostData(); + std::string ParseMultipartFormData(); void MakeRequest(); void MakeRequestNonSSL(httplib::Request& request, const URLInfo& url_info, std::vector& pending_headers); void MakeRequestSSL(httplib::Request& request, const URLInfo& url_info, std::vector& pending_headers); + bool ContentProvider(size_t offset, size_t length, httplib::DataSink& sink); + bool ChunkedContentProvider(size_t offset, httplib::DataSink& sink); + std::size_t HandleHeaderWrite(std::vector& pending_headers, + httplib::Stream& strm, httplib::Headers& httplib_headers); }; struct SessionData : public Kernel::SessionRequestHandler::SessionDataBase { @@ -308,6 +370,16 @@ private: */ void CancelConnection(Kernel::HLERequestContext& ctx); + /** + * HTTP_C::GetRequestState service function + * Inputs: + * 1 : Context handle + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Request state + */ + void GetRequestState(Kernel::HLERequestContext& ctx); + /** * HTTP_C::GetDownloadSizeState service function * Inputs: @@ -418,6 +490,21 @@ private: */ void AddPostDataAscii(Kernel::HLERequestContext& ctx); + /** + * HTTP_C::AddPostDataBinary service function + * Inputs: + * 1 : Context handle + * 2 : Form name buffer size, including null-terminator. + * 3 : Form value buffer size + * 4 : (FormNameSize<<14) | 0xC02 + * 5 : Form name data pointer + * 6 : (FormValueSize<<4) | 10 + * 7 : Form value data pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void AddPostDataBinary(Kernel::HLERequestContext& ctx); + /** * HTTP_C::AddPostDataRaw service function * Inputs: @@ -430,6 +517,140 @@ private: */ void AddPostDataRaw(Kernel::HLERequestContext& ctx); + /** + * HTTP_C::SetPostDataType service function + * Inputs: + * 1 : Context handle + * 2 : Post data type + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetPostDataType(Kernel::HLERequestContext& ctx); + + /** + * HTTP_C::SendPostDataAscii service function + * Inputs: + * 1 : Context handle + * 2 : Form name buffer size, including null-terminator. + * 3 : Form value buffer size, including null-terminator. + * 4 : (FormNameSize<<14) | 0xC02 + * 5 : Form name data pointer + * 6 : (FormValueSize<<4) | 10 + * 7 : Form value data pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SendPostDataAscii(Kernel::HLERequestContext& ctx); + + /** + * HTTP_C::SendPostDataAsciiTimeout service function + * Inputs: + * 1 : Context handle + * 2 : Form name buffer size, including null-terminator. + * 3 : Form value buffer size, including null-terminator. + * 4-5 : u64 nanoseconds delay + * 6 : (FormNameSize<<14) | 0xC02 + * 7 : Form name data pointer + * 8 : (FormValueSize<<4) | 10 + * 9 : Form value data pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SendPostDataAsciiTimeout(Kernel::HLERequestContext& ctx); + + /** + * SendPostDataAsciiImpl: + * Implements SendPostDataAscii and SendPostDataAsciiTimeout service functions + */ + void SendPostDataAsciiImpl(Kernel::HLERequestContext& ctx, bool timeout); + + /** + * HTTP_C::SendPostDataBinary service function + * Inputs: + * 1 : Context handle + * 2 : Form name buffer size, including null-terminator. + * 3 : Form value buffer size + * 4 : (FormNameSize<<14) | 0xC02 + * 5 : Form name data pointer + * 6 : (FormValueSize<<4) | 10 + * 7 : Form value data pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SendPostDataBinary(Kernel::HLERequestContext& ctx); + + /** + * HTTP_C::SendPostDataBinaryTimeout service function + * Inputs: + * 1 : Context handle + * 2 : Form name buffer size, including null-terminator. + * 3 : Form value buffer size + * 4-5 : u64 nanoseconds delay + * 6 : (FormNameSize<<14) | 0xC02 + * 7 : Form name data pointer + * 8 : (FormValueSize<<4) | 10 + * 9 : Form value data pointer + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SendPostDataBinaryTimeout(Kernel::HLERequestContext& ctx); + + /** + * SendPostDataBinaryImpl: + * Implements SendPostDataBinary and SendPostDataBinaryTimeout service functions + */ + void SendPostDataBinaryImpl(Kernel::HLERequestContext& ctx, bool timeout); + + /** + * HTTP_C::SendPostDataRaw service function + * Inputs: + * 1 : Context handle + * 2 : Post data length + * 3-4: (Mapped buffer) Post data + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + * 2-3: (Mapped buffer) Post data + */ + void SendPostDataRaw(Kernel::HLERequestContext& ctx); + + /** + * HTTP_C::SendPostDataRawTimeout service function + * Inputs: + * 1 : Context handle + * 2 : Post data length + * 3-4: u64 nanoseconds delay + * 5-6: (Mapped buffer) Post data + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + * 2-3: (Mapped buffer) Post data + */ + void SendPostDataRawTimeout(Kernel::HLERequestContext& ctx); + + /** + * SendPostDataRawImpl: + * Implements SendPostDataRaw and SendPostDataRawTimeout service functions + */ + void SendPostDataRawImpl(Kernel::HLERequestContext& ctx, bool timeout); + + /** + * HTTP_C::NotifyFinishSendPostData service function + * Inputs: + * 1 : Context handle + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void NotifyFinishSendPostData(Kernel::HLERequestContext& ctx); + + /** + * HTTP_C::SetPostDataEncoding service function + * Inputs: + * 1 : Context handle + * 2 : Post data encoding + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetPostDataEncoding(Kernel::HLERequestContext& ctx); + /** * HTTP_C::GetResponseHeader service function * Inputs: @@ -445,6 +666,28 @@ private: */ void GetResponseHeader(Kernel::HLERequestContext& ctx); + /** + * HTTP_C::GetResponseHeaderTimeout service function + * Inputs: + * 1 : Context handle + * 2 : Header name length + * 3 : Return value length + * 4-5 : u64 nanoseconds delay + * 6-7 : (Static buffer) Header name + * 8-9 : (Mapped buffer) Header value + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + * 2 : Header value copied size + * 3-4: (Mapped buffer) Header value + */ + void GetResponseHeaderTimeout(Kernel::HLERequestContext& ctx); + + /** + * GetResponseHeaderImpl: + * Implements GetResponseHeader and GetResponseHeaderTimeout service functions + */ + void GetResponseHeaderImpl(Kernel::HLERequestContext& ctx, bool timeout); + /** * HTTP_C::GetResponseStatusCode service function * Inputs: @@ -578,6 +821,17 @@ private: */ void SetKeepAlive(Kernel::HLERequestContext& ctx); + /** + * HTTP_C::SetPostDataTypeSize service function + * Inputs: + * 1 : Context handle + * 2 : Post data type + * 3 : Content length size + * Outputs: + * 1 : Result of function, 0 on success, otherwise error code + */ + void SetPostDataTypeSize(Kernel::HLERequestContext& ctx); + /** * HTTP_C::Finalize service function * Outputs: