| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | DockerHTTPClient.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains the implementation of the Docker HTTP client. |
| 12 | This class is designed to wrap calls to the docker API over a socket channel. |
| 13 | |
| 14 | The flow of an HTTP request is: |
| 15 | |
| 16 | - Create a new hvsocket channel by sending a WSLC_FORK message to init. |
| 17 | - Connect the new socket to the docker unix socket server via WSLC_UNIX_CONNECT |
| 18 | - Once connected, send the HTTP request over that socket. |
| 19 | |
| 20 | Some HTTP requests have simple response bodies that can be read right away, and some others upgrade |
| 21 | the connection to TCP (like attaching to a process stdio, importing a tar, ...). For those, |
| 22 | we return the socket without reading the response body, so the caller can interact directly with the stream. |
| 23 | |
| 24 | --*/ |
| 25 | |
| 26 | #include "precomp.h" |
| 27 | |
| 28 | #include <winrt/Windows.Foundation.h> |
| 29 | #include "DockerHTTPClient.h" |
| 30 | |
| 31 | namespace http = boost::beast::http; |
| 32 | using boost::beast::http::verb; |
| 33 | using wsl::windows::common::docker_schema::EmptyRequest; |
| 34 | using wsl::windows::common::io::HandleWrapper; |
| 35 | using wsl::windows::common::io::MultiHandleWait; |
| 36 | using wsl::windows::service::wslc::DockerHTTPClient; |
| 37 | using namespace wsl::windows::common; |
| 38 | |
| 39 | namespace { |
| 40 | |
| 41 | bool IsResponseChunked(const http::response_parser<http::buffer_body>::value_type& response) |
| 42 | { |
| 43 | auto transferEncoding = response.find(http::field::transfer_encoding); |
| 44 | if (transferEncoding == response.end()) |
| 45 | { |
| 46 | return false; |
| 47 | } |
| 48 | |
| 49 | if (transferEncoding->value() != "chunked") |
| 50 | { |
| 51 | THROW_HR_MSG(E_UNEXPECTED, "Unknown transfer encoding: %hs", std::string(transferEncoding->value()).c_str()); |
| 52 | } |
| 53 | |
| 54 | return true; |
| 55 | } |
| 56 | |
| 57 | } // namespace |
| 58 | |
| 59 | std::string wsl::windows::service::wslc::FormatDockerEngineError(const std::string& EngineMessage) |
| 60 | { |
| 61 | if (EngineMessage.empty() || wsl::shared::Localization::IsCurrentLanguageEnglish()) |
| 62 | { |
| 63 | return EngineMessage; |
| 64 | } |
| 65 | |
| 66 | return wsl::shared::string::WideToMultiByte(wsl::shared::Localization::MessageWslcDockerEngineErrorPrefix()) + " " + EngineMessage; |
| 67 | } |
| 68 | |
| 69 | DockerHTTPClient::URL::URL(std::string&& Path) : m_path(std::move(Path)) |
| 70 | { |
| 71 | } |
| 72 | |
| 73 | void DockerHTTPClient::URL::SetParameter(std::string&& Key, std::string&& Value) |
| 74 | { |
| 75 | m_parameters.emplace(std::move(Key), std::move(Value)); |
| 76 | } |
| 77 | |
| 78 | void DockerHTTPClient::URL::SetParameter(std::string&& Key, const std::string& Value) |
| 79 | { |
| 80 | m_parameters.emplace(std::move(Key), Value); |
| 81 | } |
| 82 | |
| 83 | void DockerHTTPClient::URL::SetParameter(std::string&& Key, const char* Value) |
| 84 | { |
| 85 | SetParameter(std::move(Key), std::string(Value)); |
| 86 | } |
| 87 | |
| 88 | void DockerHTTPClient::URL::SetParameter(std::string&& Key, bool Value) |
| 89 | { |
| 90 | m_parameters.emplace(std::move(Key), Value ? "true" : "false"); |
| 91 | } |
| 92 | |
| 93 | std::string DockerHTTPClient::URL::Get() const |
| 94 | { |
| 95 | constexpr auto urlPrefix = "http://localhost"; |
| 96 | |
| 97 | std::stringstream url; |
| 98 | url << urlPrefix; |
| 99 | url << m_path; |
| 100 | |
| 101 | if (!m_parameters.empty()) |
| 102 | { |
| 103 | url << "?"; |
| 104 | bool first = true; |
| 105 | for (const auto& [key, value] : m_parameters) |
| 106 | { |
| 107 | if (!first) |
| 108 | { |
| 109 | url << "&"; |
| 110 | } |
| 111 | |
| 112 | url << key << "=" << Escape(value); |
| 113 | first = false; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | return url.str(); |
| 118 | } |
| 119 | |
| 120 | std::string DockerHTTPClient::URL::Escape(const std::string& Value) |
| 121 | { |
| 122 | auto escaped = winrt::Windows::Foundation::Uri::EscapeComponent(winrt::to_hstring(Value)); |
| 123 | |
| 124 | return wsl::shared::string::WideToMultiByte(escaped.c_str()); |
| 125 | } |
| 126 | |
| 127 | DockerHTTPClient::DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE exitingEvent, GUID VmId, ULONG ConnectTimeoutMs) : |
| 128 | m_exitingEvent(exitingEvent), m_channel(std::move(Channel)), m_vmId(VmId), m_connectTimeoutMs(ConnectTimeoutMs) |
| 129 | { |
| 130 | } |
| 131 | |
| 132 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PullImage( |
| 133 | const std::string& Repo, const std::optional<std::string>& tagOrDigest, const std::optional<std::string>& registryAuth) |
| 134 | { |
| 135 | auto url = URL::Create("/images/create"); |
| 136 | |
| 137 | // Normalize the repo server & path |
| 138 | url.SetParameter("fromImage", wslutil::RepositoryReference::Parse(Repo).GetCanonical()); |
| 139 | |
| 140 | if (tagOrDigest.has_value()) |
| 141 | { |
| 142 | url.SetParameter("tag", tagOrDigest.value()); |
| 143 | } |
| 144 | |
| 145 | std::map<std::string, std::string> customHeaders; |
| 146 | |
| 147 | if (registryAuth.has_value()) |
| 148 | { |
| 149 | customHeaders["X-Registry-Auth"] = registryAuth.value(); |
| 150 | } |
| 151 | |
| 152 | return SendRequestImpl(verb::post, url, {}, customHeaders); |
| 153 | } |
| 154 | |
| 155 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::LoadImage(uint64_t ContentLength) |
| 156 | { |
| 157 | return SendRequestImpl( |
| 158 | verb::post, URL::Create("/images/load"), {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); |
| 159 | } |
| 160 | |
| 161 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength) |
| 162 | { |
| 163 | auto url = URL::Create("/images/create"); |
| 164 | url.SetParameter("tag", Tag); |
| 165 | url.SetParameter("repo", Repo); |
| 166 | url.SetParameter("fromSrc", "-"); |
| 167 | |
| 168 | return SendRequestImpl(verb::post, url, {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); |
| 169 | } |
| 170 | |
| 171 | void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag) |
| 172 | { |
| 173 | auto url = URL::Create("/images/{}/tag", Id); |
| 174 | url.SetParameter("repo", Repo); |
| 175 | url.SetParameter("tag", Tag); |
| 176 | |
| 177 | Transaction<docker_schema::EmptyRequest>(verb::post, url); |
| 178 | } |
| 179 | |
| 180 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PushImage( |
| 181 | const std::string& ImageName, const std::optional<std::string>& tag, const std::string& registryAuth) |
| 182 | { |
| 183 | auto url = URL::Create("/images/{}/push", ImageName); |
| 184 | |
| 185 | if (tag.has_value()) |
| 186 | { |
| 187 | url.SetParameter("tag", tag.value()); |
| 188 | } |
| 189 | |
| 190 | std::map<std::string, std::string> customHeaders = {{"X-Registry-Auth", registryAuth}}; |
| 191 | return SendRequestImpl(verb::post, url, {}, customHeaders); |
| 192 | } |
| 193 | |
| 194 | std::string DockerHTTPClient::Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password) |
| 195 | { |
| 196 | auto response = Transaction<docker_schema::AuthRequest>( |
| 197 | verb::post, URL::Create("/auth"), {.username = username, .password = password, .serveraddress = serverAddress}); |
| 198 | |
| 199 | return response.IdentityToken.value_or(""); |
| 200 | } |
| 201 | |
| 202 | std::vector<docker_schema::Image> DockerHTTPClient::ListImages(bool all, bool digests, const std::map<std::string, std::vector<std::string>>& filters) |
| 203 | { |
| 204 | auto url = URL::Create("/images/json"); |
| 205 | |
| 206 | url.SetParameter("all", all); |
| 207 | url.SetParameter("digests", digests); |
| 208 | |
| 209 | if (!filters.empty()) |
| 210 | { |
| 211 | nlohmann::json filtersJson = filters; |
| 212 | url.SetParameter("filters", filtersJson.dump()); |
| 213 | } |
| 214 | |
| 215 | return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Image>>(verb::get, url); |
| 216 | } |
| 217 | |
| 218 | docker_schema::InspectImage DockerHTTPClient::InspectImage(const std::string& NameOrId) |
| 219 | { |
| 220 | return Transaction<docker_schema::EmptyRequest, docker_schema::InspectImage>(verb::get, URL::Create("/images/{}/json", NameOrId)); |
| 221 | } |
| 222 | |
| 223 | std::vector<docker_schema::DeletedImage> wsl::windows::service::wslc::DockerHTTPClient::DeleteImage(const char* Image, bool Force, bool NoPrune) |
| 224 | { |
| 225 | auto url = URL::Create("/images/{}", Image); |
| 226 | url.SetParameter("force", Force); |
| 227 | url.SetParameter("noprune", NoPrune); |
| 228 | |
| 229 | return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::DeletedImage>>(verb::delete_, url); |
| 230 | } |
| 231 | |
| 232 | std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImage(const std::string& NameOrId) |
| 233 | { |
| 234 | auto [response, socket] = SendRequest(verb::get, URL::Create("/images/{}/get", NameOrId), {}, {}); |
| 235 | |
| 236 | return {response.result_int(), std::move(socket)}; |
| 237 | } |
| 238 | |
| 239 | std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::SaveImages(const std::vector<std::string>& NamesOrIds) |
| 240 | { |
| 241 | auto url = URL::Create("/images/get"); |
| 242 | for (const auto& name : NamesOrIds) |
| 243 | { |
| 244 | // 'names' is a repeated query parameter. |
| 245 | // See: https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageGet |
| 246 | url.SetParameter("names", name); |
| 247 | } |
| 248 | |
| 249 | auto [response, socket] = SendRequest(verb::get, url, {}, {}); |
| 250 | |
| 251 | return {response.result_int(), std::move(socket)}; |
| 252 | } |
| 253 | |
| 254 | docker_schema::PruneImageResult DockerHTTPClient::PruneImages(const std::map<std::string, std::vector<std::string>>& filters) |
| 255 | { |
| 256 | auto url = URL::Create("/images/prune"); |
| 257 | |
| 258 | if (!filters.empty()) |
| 259 | { |
| 260 | nlohmann::json filtersJson = filters; |
| 261 | url.SetParameter("filters", filtersJson.dump()); |
| 262 | } |
| 263 | |
| 264 | return Transaction<docker_schema::EmptyRequest, docker_schema::PruneImageResult>(verb::post, url); |
| 265 | } |
| 266 | |
| 267 | std::vector<docker_schema::ContainerInfo> DockerHTTPClient::ListContainers( |
| 268 | bool all, int limit, const std::map<std::string, std::vector<std::string>>& filters) |
| 269 | { |
| 270 | auto url = URL::Create("/containers/json"); |
| 271 | url.SetParameter("all", all); |
| 272 | url.SetParameter("limit", std::to_string(limit)); |
| 273 | |
| 274 | if (!filters.empty()) |
| 275 | { |
| 276 | nlohmann::json filtersJson = filters; |
| 277 | url.SetParameter("filters", filtersJson.dump()); |
| 278 | } |
| 279 | |
| 280 | return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::ContainerInfo>>(verb::get, url); |
| 281 | } |
| 282 | |
| 283 | docker_schema::CreatedContainer DockerHTTPClient::CreateContainer(const docker_schema::CreateContainer& Request, const std::optional<std::string>& Name) |
| 284 | { |
| 285 | auto url = URL::Create("/containers/create"); |
| 286 | if (Name.has_value()) |
| 287 | { |
| 288 | url.SetParameter("name", Name.value()); |
| 289 | } |
| 290 | |
| 291 | return Transaction<docker_schema::CreateContainer>(verb::post, url, Request); |
| 292 | } |
| 293 | |
| 294 | void DockerHTTPClient::ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns) |
| 295 | { |
| 296 | auto url = URL::Create("/containers/{}/resize", Id); |
| 297 | url.SetParameter("w", std::to_string(Columns)); |
| 298 | url.SetParameter("h", std::to_string(Rows)); |
| 299 | |
| 300 | Transaction(verb::post, url); |
| 301 | } |
| 302 | |
| 303 | void DockerHTTPClient::StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys) |
| 304 | { |
| 305 | auto url = URL::Create("/containers/{}/start", Id); |
| 306 | if (DetachKeys.has_value()) |
| 307 | { |
| 308 | url.SetParameter("detachKeys", DetachKeys.value()); |
| 309 | } |
| 310 | |
| 311 | Transaction(verb::post, url); |
| 312 | } |
| 313 | |
| 314 | void DockerHTTPClient::StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<LONG> TimeoutSeconds) |
| 315 | { |
| 316 | auto url = URL::Create("/containers/{}/stop", Id); |
| 317 | if (Signal.has_value()) |
| 318 | { |
| 319 | url.SetParameter("signal", std::to_string(static_cast<int>(Signal.value()))); |
| 320 | } |
| 321 | |
| 322 | if (TimeoutSeconds.has_value()) |
| 323 | { |
| 324 | url.SetParameter("t", std::to_string(TimeoutSeconds.value())); |
| 325 | } |
| 326 | |
| 327 | Transaction(verb::post, url); |
| 328 | } |
| 329 | |
| 330 | void DockerHTTPClient::SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal) |
| 331 | { |
| 332 | auto url = URL::Create("/containers/{}/kill", Id); |
| 333 | if (Signal.has_value()) |
| 334 | { |
| 335 | url.SetParameter("signal", std::to_string(static_cast<int>(Signal.value()))); |
| 336 | } |
| 337 | |
| 338 | Transaction(verb::post, url); |
| 339 | } |
| 340 | |
| 341 | void DockerHTTPClient::DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes) |
| 342 | { |
| 343 | auto url = URL::Create("/containers/{}", Id); |
| 344 | |
| 345 | if (Force) |
| 346 | { |
| 347 | url.SetParameter("force", true); |
| 348 | } |
| 349 | |
| 350 | if (DeleteVolumes) |
| 351 | { |
| 352 | url.SetParameter("v", true); |
| 353 | } |
| 354 | |
| 355 | Transaction(verb::delete_, url); |
| 356 | } |
| 357 | |
| 358 | docker_schema::InspectContainer DockerHTTPClient::InspectContainer(const std::string& Id, bool Size) |
| 359 | { |
| 360 | auto url = URL::Create("/containers/{}/json", Id); |
| 361 | url.SetParameter("size", Size); |
| 362 | |
| 363 | return Transaction<EmptyRequest, docker_schema::InspectContainer>(verb::get, url); |
| 364 | } |
| 365 | |
| 366 | docker_schema::ContainerStats DockerHTTPClient::ContainerStats(const std::string& Id) |
| 367 | { |
| 368 | auto url = URL::Create("/containers/{}/stats", Id); |
| 369 | url.SetParameter("stream", false); |
| 370 | |
| 371 | // Intentionally omit one-shot=true: the Docker engine blocks internally for ~1s |
| 372 | // to collect a prior sample, and returns a single response with both cpu_stats and |
| 373 | // precpu_stats correctly populated — giving a valid delta for CPU % calculation. |
| 374 | return Transaction<EmptyRequest, docker_schema::ContainerStats>(verb::get, url); |
| 375 | } |
| 376 | |
| 377 | docker_schema::InspectExec DockerHTTPClient::InspectExec(const std::string& Id) |
| 378 | { |
| 379 | return Transaction<EmptyRequest, docker_schema::InspectExec>(verb::get, URL::Create("/exec/{}/json", Id)); |
| 380 | } |
| 381 | |
| 382 | wil::unique_socket DockerHTTPClient::AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys) |
| 383 | { |
| 384 | std::map<std::string, std::string> headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}}; |
| 385 | |
| 386 | auto url = URL::Create("/containers/{}/attach", Id); |
| 387 | url.SetParameter("stream", true); |
| 388 | url.SetParameter("stdin", true); |
| 389 | url.SetParameter("stdout", true); |
| 390 | url.SetParameter("stderr", true); |
| 391 | |
| 392 | if (DetachKeys.has_value()) |
| 393 | { |
| 394 | url.SetParameter("detachKeys", DetachKeys.value()); |
| 395 | } |
| 396 | |
| 397 | auto [response, socket] = SendRequest(verb::post, url, {}, headers); |
| 398 | |
| 399 | if (response.result_int() != 101) |
| 400 | { |
| 401 | throw DockerHTTPException(std::move(response), verb::post, url.Get(), "", ""); |
| 402 | } |
| 403 | |
| 404 | return std::move(socket); |
| 405 | } |
| 406 | |
| 407 | std::pair<uint32_t, wil::unique_socket> DockerHTTPClient::ExportContainer(const std::string& ContainerNameOrId) |
| 408 | { |
| 409 | auto [response, socket] = SendRequest(verb::get, URL::Create("/containers/{}/export", ContainerNameOrId), {}, {}); |
| 410 | |
| 411 | return {response.result_int(), std::move(socket)}; |
| 412 | } |
| 413 | |
| 414 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PutArchive( |
| 415 | const std::string& ContainerID, const std::string& Path, std::optional<uint64_t> ContentLength) |
| 416 | { |
| 417 | auto url = URL::Create("/containers/{}/archive", ContainerID); |
| 418 | url.SetParameter("path", Path); |
| 419 | |
| 420 | std::map<std::string, std::string> headers = {{"Content-Type", "application/x-tar"}}; |
| 421 | if (ContentLength.has_value()) |
| 422 | { |
| 423 | headers["Content-Length"] = std::to_string(ContentLength.value()); |
| 424 | } |
| 425 | |
| 426 | return SendRequestImpl(verb::put, url, {}, headers); |
| 427 | } |
| 428 | |
| 429 | std::tuple<uint32_t, wil::unique_socket, bool> DockerHTTPClient::GetArchive(const std::string& ContainerID, const std::string& Path) |
| 430 | { |
| 431 | auto url = URL::Create("/containers/{}/archive", ContainerID); |
| 432 | url.SetParameter("path", Path); |
| 433 | |
| 434 | auto [response, socket] = SendRequest(verb::get, url, {}, {}); |
| 435 | |
| 436 | return {response.result_int(), std::move(socket), response.chunked()}; |
| 437 | } |
| 438 | |
| 439 | docker_schema::Volume DockerHTTPClient::CreateVolume(const docker_schema::CreateVolume& Request) |
| 440 | { |
| 441 | return Transaction<docker_schema::CreateVolume>(verb::post, URL::Create("/volumes/create"), Request); |
| 442 | } |
| 443 | |
| 444 | docker_schema::Volume DockerHTTPClient::InspectVolume(const std::string& Name) |
| 445 | { |
| 446 | return Transaction<docker_schema::EmptyRequest, docker_schema::Volume>(verb::get, URL::Create("/volumes/{}", Name)); |
| 447 | } |
| 448 | |
| 449 | void DockerHTTPClient::RemoveVolume(const std::string& Name) |
| 450 | { |
| 451 | Transaction(verb::delete_, URL::Create("/volumes/{}", Name)); |
| 452 | } |
| 453 | |
| 454 | std::vector<docker_schema::Volume> DockerHTTPClient::ListVolumes(const std::map<std::string, std::vector<std::string>>& filters) |
| 455 | { |
| 456 | auto url = URL::Create("/volumes"); |
| 457 | |
| 458 | if (!filters.empty()) |
| 459 | { |
| 460 | url.SetParameter("filters", nlohmann::json(filters).dump()); |
| 461 | } |
| 462 | |
| 463 | auto response = Transaction<docker_schema::EmptyRequest, docker_schema::ListVolumesResponse>(verb::get, url); |
| 464 | return response.Volumes; |
| 465 | } |
| 466 | |
| 467 | docker_schema::PruneVolumeResult DockerHTTPClient::PruneVolumes(const std::map<std::string, std::vector<std::string>>& filters) |
| 468 | { |
| 469 | auto url = URL::Create("/volumes/prune"); |
| 470 | |
| 471 | if (!filters.empty()) |
| 472 | { |
| 473 | url.SetParameter("filters", nlohmann::json(filters).dump()); |
| 474 | } |
| 475 | |
| 476 | return Transaction<docker_schema::EmptyRequest, docker_schema::PruneVolumeResult>(verb::post, url); |
| 477 | } |
| 478 | |
| 479 | docker_schema::CreateNetworkResponse DockerHTTPClient::CreateNetwork(const docker_schema::CreateNetwork& Request) |
| 480 | { |
| 481 | return Transaction(verb::post, URL::Create("/networks/create"), Request); |
| 482 | } |
| 483 | |
| 484 | void DockerHTTPClient::RemoveNetwork(const std::string& Name) |
| 485 | { |
| 486 | Transaction(verb::delete_, URL::Create("/networks/{}", Name)); |
| 487 | } |
| 488 | |
| 489 | void DockerHTTPClient::ConnectContainerToNetwork(const std::string& NetworkName, const docker_schema::ContainerNetworkRequest& Request) |
| 490 | { |
| 491 | Transaction(verb::post, URL::Create("/networks/{}/connect", NetworkName), Request); |
| 492 | } |
| 493 | |
| 494 | void DockerHTTPClient::DisconnectContainerFromNetwork(const std::string& NetworkName, const docker_schema::ContainerNetworkRequest& Request) |
| 495 | { |
| 496 | Transaction(verb::post, URL::Create("/networks/{}/disconnect", NetworkName), Request); |
| 497 | } |
| 498 | |
| 499 | std::vector<docker_schema::Network> DockerHTTPClient::ListNetworks(const std::map<std::string, std::vector<std::string>>& filters) |
| 500 | { |
| 501 | auto url = URL::Create("/networks"); |
| 502 | |
| 503 | if (!filters.empty()) |
| 504 | { |
| 505 | url.SetParameter("filters", nlohmann::json(filters).dump()); |
| 506 | } |
| 507 | |
| 508 | return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Network>>(verb::get, url); |
| 509 | } |
| 510 | |
| 511 | docker_schema::Network DockerHTTPClient::InspectNetwork(const std::string& Name) |
| 512 | { |
| 513 | return Transaction<docker_schema::EmptyRequest, docker_schema::Network>(verb::get, URL::Create("/networks/{}", Name)); |
| 514 | } |
| 515 | |
| 516 | docker_schema::PruneNetworkResult DockerHTTPClient::PruneNetworks(const std::map<std::string, std::vector<std::string>>& filters) |
| 517 | { |
| 518 | auto url = URL::Create("/networks/prune"); |
| 519 | |
| 520 | if (!filters.empty()) |
| 521 | { |
| 522 | url.SetParameter("filters", nlohmann::json(filters).dump()); |
| 523 | } |
| 524 | |
| 525 | return Transaction<docker_schema::EmptyRequest, docker_schema::PruneNetworkResult>(verb::post, url); |
| 526 | } |
| 527 | |
| 528 | wil::unique_socket DockerHTTPClient::ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) |
| 529 | { |
| 530 | auto url = URL::Create("/containers/{}/logs", Id); |
| 531 | url.SetParameter("follow", WI_IsFlagSet(Flags, WSLCLogsFlagsFollow)); |
| 532 | url.SetParameter("stdout", true); |
| 533 | url.SetParameter("stderr", true); |
| 534 | url.SetParameter("timestamps", WI_IsFlagSet(Flags, WSLCLogsFlagsTimestamps)); |
| 535 | |
| 536 | if (Tail != 0) |
| 537 | { |
| 538 | url.SetParameter("tail", std::to_string(Tail)); |
| 539 | } |
| 540 | |
| 541 | if (Until != 0) |
| 542 | { |
| 543 | url.SetParameter("until", std::to_string(Until)); |
| 544 | } |
| 545 | |
| 546 | if (Since != 0) |
| 547 | { |
| 548 | url.SetParameter("since", std::to_string(Since)); |
| 549 | } |
| 550 | |
| 551 | auto [response, socket] = SendRequest(verb::get, url, {}, {}); |
| 552 | if (response.result_int() != 200) |
| 553 | { |
| 554 | throw DockerHTTPException(std::move(response), verb::get, url.Get(), "", ""); |
| 555 | } |
| 556 | |
| 557 | return std::move(socket); |
| 558 | } |
| 559 | |
| 560 | docker_schema::PruneContainerResult DockerHTTPClient::PruneContainers(const std::map<std::string, std::vector<std::string>>& filters) |
| 561 | { |
| 562 | auto url = URL::Create("/containers/prune"); |
| 563 | |
| 564 | if (!filters.empty()) |
| 565 | { |
| 566 | nlohmann::json filtersJson = filters; |
| 567 | url.SetParameter("filters", filtersJson.dump()); |
| 568 | } |
| 569 | |
| 570 | return Transaction<docker_schema::EmptyRequest, docker_schema::PruneContainerResult>(verb::post, url); |
| 571 | } |
| 572 | |
| 573 | docker_schema::CreateExecResponse DockerHTTPClient::CreateExec(const std::string& Container, const docker_schema::CreateExec& Request) |
| 574 | { |
| 575 | return Transaction<docker_schema::CreateExec>(verb::post, URL::Create("/containers/{}/exec", Container), Request); |
| 576 | } |
| 577 | |
| 578 | wil::unique_socket DockerHTTPClient::StartExec(const std::string& Id, const common::docker_schema::StartExec& Request) |
| 579 | { |
| 580 | std::map<std::string, std::string> headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}}; |
| 581 | |
| 582 | auto url = URL::Create("/exec/{}/start", Id); |
| 583 | |
| 584 | auto body = wsl::shared::ToJson(Request); |
| 585 | auto [response, socket] = SendRequest(verb::post, url, body, headers); |
| 586 | if (response.result_int() != 101) |
| 587 | { |
| 588 | throw DockerHTTPException(std::move(response), verb::post, url.Get(), std::move(body), ""); |
| 589 | } |
| 590 | return std::move(socket); |
| 591 | } |
| 592 | |
| 593 | void DockerHTTPClient::ResizeExecTty(const std::string& Id, ULONG Rows, ULONG Columns) |
| 594 | { |
| 595 | auto url = URL::Create("/exec/{}/resize", Id); |
| 596 | url.SetParameter("w", std::to_string(Columns)); |
| 597 | url.SetParameter("h", std::to_string(Rows)); |
| 598 | |
| 599 | Transaction(verb::post, url); |
| 600 | } |
| 601 | |
| 602 | wil::unique_socket DockerHTTPClient::MonitorEvents() |
| 603 | { |
| 604 | auto url = URL::Create("/events"); |
| 605 | auto [response, socket] = SendRequest(verb::get, url, {}); |
| 606 | |
| 607 | if (response.result_int() != 200) |
| 608 | { |
| 609 | throw DockerHTTPException(std::move(response), verb::get, url.Get(), "", ""); |
| 610 | } |
| 611 | |
| 612 | return std::move(socket); |
| 613 | } |
| 614 | |
| 615 | wil::unique_socket DockerHTTPClient::ConnectSocket() |
| 616 | { |
| 617 | auto lock = m_lock.lock_exclusive(); |
| 618 | |
| 619 | // Send a fork message. |
| 620 | WSLC_FORK message; |
| 621 | message.ForkType = WSLC_FORK::Thread; |
| 622 | const auto& response = m_channel.Transaction(message); |
| 623 | |
| 624 | THROW_HR_IF_MSG(E_FAIL, response.Pid <= 0, "fork() returned %i", response.Pid); |
| 625 | |
| 626 | // Connect the new hvsocket. |
| 627 | wsl::shared::SocketChannel newChannel{ |
| 628 | wsl::windows::common::hvsocket::Connect(m_vmId, response.Port, m_exitingEvent, m_connectTimeoutMs), "DockerClient", {m_exitingEvent}}; |
| 629 | lock.reset(); |
| 630 | |
| 631 | // Connect that socket to the docker unix socket. |
| 632 | shared::MessageWriter<WSLC_UNIX_CONNECT> writer; |
| 633 | writer.WriteString(writer->PathOffset, "/var/run/docker.sock"); |
| 634 | |
| 635 | auto result = newChannel.Transaction<WSLC_UNIX_CONNECT>(writer.Span()); |
| 636 | THROW_HR_IF_MSG(E_FAIL, result.Result < 0, "Failed to connect to unix socket: '/var/run/docker.sock', %i", result.Result); |
| 637 | |
| 638 | return newChannel.Release(); |
| 639 | } |
| 640 | |
| 641 | std::pair<DockerHTTPClient::HTTPResponse, std::string> DockerHTTPClient::SendRequestAndReadResponse(verb Method, const URL& Url, const std::string& Body) |
| 642 | { |
| 643 | // Send the request. |
| 644 | auto context = SendRequestImpl(Method, Url, Body, {}); |
| 645 | |
| 646 | // Read the response header and body. |
| 647 | // Limit response size to prevent unbounded memory growth from pathological responses. |
| 648 | // All callers expect JSON metadata (list, inspect, create, etc.), not large binary payloads. |
| 649 | constexpr size_t MaxResponseSize = 64 * _1MB; |
| 650 | |
| 651 | std::optional<HTTPResponse> responseHeader; |
| 652 | std::string responseBody; |
| 653 | const auto& url = Url; |
| 654 | auto OnResponse = [&responseBody, &url](const gsl::span<char>& span) { |
| 655 | THROW_HR_IF_MSG( |
| 656 | HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE), |
| 657 | span.size() > MaxResponseSize - responseBody.size(), |
| 658 | "Docker API response exceeds maximum size (%zu bytes) for %hs", |
| 659 | MaxResponseSize, |
| 660 | url.Get().c_str()); |
| 661 | responseBody.append(span.data(), span.size()); |
| 662 | }; |
| 663 | |
| 664 | auto onHttpResponse = [&](const auto& response) { responseHeader = response; }; |
| 665 | MultiHandleWait io; |
| 666 | |
| 667 | io.AddHandle(std::make_unique<io::EventHandle>(m_exitingEvent, [&]() { THROW_HR(E_ABORT); })); |
| 668 | io.AddHandle(std::make_unique<DockerHttpResponseHandle>(*context, std::move(onHttpResponse), std::move(OnResponse)), MultiHandleWait::CancelOnCompleted); |
| 669 | |
| 670 | io.Run({}); |
| 671 | |
| 672 | THROW_HR_IF(E_UNEXPECTED, !responseHeader.has_value()); |
| 673 | |
| 674 | return {std::move(responseHeader.value()), responseBody}; |
| 675 | } |
| 676 | |
| 677 | DockerHTTPClient::DockerHttpResponseHandle::DockerHttpResponseHandle( |
| 678 | HTTPRequestContext& context, |
| 679 | std::function<void(const HTTPResponse&)>&& onResponseHeader, |
| 680 | std::function<void(const gsl::span<char>&)>&& onResponseBytes, |
| 681 | std::function<void()>&& onCompleted) : |
| 682 | common::io::ReadHandle( |
| 683 | HandleWrapper{context.stream.native_handle()}, std::bind(&DockerHttpResponseHandle::OnRead, this, std::placeholders::_1)), |
| 684 | Context(context), |
| 685 | OnResponseHeader(std::move(onResponseHeader)), |
| 686 | OnResponse(std::move(onResponseBytes)), |
| 687 | OnCompleted(std::move(onCompleted)) |
| 688 | { |
| 689 | } |
| 690 | |
| 691 | DockerHTTPClient::DockerHttpResponseHandle::~DockerHttpResponseHandle() |
| 692 | { |
| 693 | if (State == common::io::IOHandleStatus::Completed) |
| 694 | { |
| 695 | OnCompleted(); |
| 696 | } |
| 697 | } |
| 698 | |
| 699 | void DockerHTTPClient::DockerHttpResponseHandle::OnRead(const gsl::span<char>& Content) |
| 700 | { |
| 701 | // If the HTTP parser is done, then these bytes are part of the response body |
| 702 | if (Parser.is_header_done()) |
| 703 | { |
| 704 | OnResponseBytes(Content); |
| 705 | } |
| 706 | else |
| 707 | { |
| 708 | // Otherwise keep parsing the HTTP response header. |
| 709 | size_t i{}; |
| 710 | for (i = 0; i < Content.size() && !HeaderEnd.IsDone(); i++) |
| 711 | { |
| 712 | HeaderEnd.Consume(Content[i]); |
| 713 | } |
| 714 | |
| 715 | // Feed the parser up to the end of the header. |
| 716 | boost::beast::error_code error; |
| 717 | Parser.put(boost::asio::buffer(Content.data(), i), error); |
| 718 | |
| 719 | THROW_HR_IF_MSG( |
| 720 | E_UNEXPECTED, error && error != boost::beast::http::error::need_more, "Error parsing HTTP response: %hs", error.what().c_str()); |
| 721 | |
| 722 | if (Parser.is_header_done()) |
| 723 | { |
| 724 | const auto& response = Parser.get(); |
| 725 | OnResponseHeader(response); |
| 726 | |
| 727 | // If the response is chunked, then create a chunked reader. |
| 728 | if (IsResponseChunked(response)) |
| 729 | { |
| 730 | ResponseParser.emplace(HandleWrapper{Context.stream.native_handle()}, std::move(OnResponse)); |
| 731 | } |
| 732 | |
| 733 | auto contentLength = response.find(http::field::content_length); |
| 734 | if (contentLength != response.end()) |
| 735 | { |
| 736 | try |
| 737 | { |
| 738 | RemainingContentLength = std::stoull(contentLength->value()); |
| 739 | } |
| 740 | catch (const std::exception&) |
| 741 | { |
| 742 | THROW_HR_MSG( |
| 743 | E_UNEXPECTED, |
| 744 | "Invalid Content-Length header: %.*hs", |
| 745 | static_cast<int>(contentLength->value().size()), |
| 746 | contentLength->value().data()); |
| 747 | } |
| 748 | } |
| 749 | } |
| 750 | |
| 751 | // If any buffer remains, then it's part of the response body. |
| 752 | auto remaining = Content.subspan(i); |
| 753 | if (!remaining.empty()) |
| 754 | { |
| 755 | WI_ASSERT(Parser.is_header_done()); |
| 756 | OnResponseBytes(remaining); |
| 757 | } |
| 758 | } |
| 759 | } |
| 760 | |
| 761 | void DockerHTTPClient::DockerHttpResponseHandle::OnResponseBytes(const gsl::span<char>& Content) |
| 762 | { |
| 763 | auto span = Content; |
| 764 | |
| 765 | // If the HTTP response had a Content-Length, make sure not to read past it. |
| 766 | if (RemainingContentLength.has_value()) |
| 767 | { |
| 768 | auto consume = std::min(span.size(), RemainingContentLength.value()); |
| 769 | |
| 770 | *RemainingContentLength -= consume; |
| 771 | if (*RemainingContentLength == 0) |
| 772 | { |
| 773 | State = common::io::IOHandleStatus::Completed; |
| 774 | } |
| 775 | |
| 776 | span = span.subspan(0, consume); |
| 777 | } |
| 778 | |
| 779 | if (ResponseParser.has_value()) |
| 780 | { |
| 781 | ResponseParser->OnRead(span); |
| 782 | } |
| 783 | else |
| 784 | { |
| 785 | OnResponse(span); |
| 786 | } |
| 787 | } |
| 788 | |
| 789 | std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::SendRequestImpl( |
| 790 | verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers) |
| 791 | { |
| 792 | auto context = std::make_unique<DockerHTTPClient::HTTPRequestContext>(ConnectSocket()); |
| 793 | |
| 794 | http::request<http::string_body> req{Method, Url.Get(), 11}; |
| 795 | if (!Body.empty()) |
| 796 | { |
| 797 | req.set(http::field::content_type, "application/json"); |
| 798 | req.body() = Body; |
| 799 | |
| 800 | // N.B. prepare_payload() overrides content-length. |
| 801 | req.prepare_payload(); |
| 802 | } |
| 803 | |
| 804 | req.set(http::field::host, "localhost"); |
| 805 | req.set(http::field::connection, "close"); |
| 806 | req.set(http::field::accept, "application/json"); |
| 807 | |
| 808 | for (const auto& [name, value] : Headers) |
| 809 | { |
| 810 | req.set(name, value); |
| 811 | } |
| 812 | |
| 813 | http::write(context->stream, req); |
| 814 | |
| 815 | #ifdef WSLC_HTTP_DEBUG |
| 816 | |
| 817 | std::ostringstream oss; |
| 818 | oss << req; |
| 819 | |
| 820 | auto requestString = oss.str(); |
| 821 | |
| 822 | WSL_LOG("HTTPRequestDebug", TraceLoggingValue(Url.Get().c_str(), "Url"), TraceLoggingValue(requestString.c_str(), "Request")); |
| 823 | |
| 824 | #endif |
| 825 | |
| 826 | return std::move(context); |
| 827 | } |
| 828 | |
| 829 | std::pair<DockerHTTPClient::HTTPResponse, wil::unique_socket> DockerHTTPClient::SendRequest( |
| 830 | verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers) |
| 831 | { |
| 832 | // Write the request |
| 833 | auto context = SendRequestImpl(Method, Url, Body, Headers); |
| 834 | |
| 835 | // Parse the response header |
| 836 | constexpr auto bufferSize = 16 * 1024; |
| 837 | // Docker response header max size. |
| 838 | constexpr size_t maxHeaderSize = _1MB; |
| 839 | size_t Offset = 0; |
| 840 | std::vector<char> buffer; |
| 841 | http::response_parser<http::buffer_body> parser; |
| 842 | parser.eager(false); |
| 843 | parser.skip(false); |
| 844 | |
| 845 | HttpHeaderEndDetector headerEnd; |
| 846 | // Consume the socket until the header end is reached |
| 847 | while (!parser.is_header_done()) |
| 848 | { |
| 849 | THROW_HR_IF_MSG( |
| 850 | HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), Offset >= maxHeaderSize, "HTTP response header exceeded %zu bytes", maxHeaderSize); |
| 851 | |
| 852 | buffer.resize(Offset + bufferSize); |
| 853 | |
| 854 | // Peek for the end of the HTTP header '\r\n' |
| 855 | auto bytesRead = common::socket::Receive( |
| 856 | context->stream.native_handle(), gsl::span(reinterpret_cast<gsl::byte*>(buffer.data() + Offset), bufferSize), m_exitingEvent, MSG_PEEK); |
| 857 | |
| 858 | THROW_HR_IF(E_ABORT, bytesRead == 0); |
| 859 | |
| 860 | // Scan only the newly peeked bytes [Offset, Offset + bytesRead) |
| 861 | size_t i = 0; |
| 862 | for (i = Offset; i < bytesRead + Offset && !headerEnd.IsDone(); i++) |
| 863 | { |
| 864 | headerEnd.Consume(buffer[i]); |
| 865 | } |
| 866 | |
| 867 | WI_ASSERT(i >= Offset); |
| 868 | const size_t toConsume = i - Offset; |
| 869 | |
| 870 | // Consume the scanned header bytes from the socket |
| 871 | bytesRead = common::socket::Receive( |
| 872 | context->stream.native_handle(), gsl::span(reinterpret_cast<gsl::byte*>(buffer.data() + Offset), toConsume), m_exitingEvent, 0); |
| 873 | THROW_HR_IF(E_ABORT, bytesRead == 0); // E_ABORT case after peek but before consume |
| 874 | THROW_HR_IF_MSG( |
| 875 | E_UNEXPECTED, static_cast<size_t>(bytesRead) != toConsume, "Short read consuming HTTP header: got %d, expected %zu", bytesRead, toConsume); |
| 876 | |
| 877 | Offset += bytesRead; |
| 878 | buffer.resize(Offset); |
| 879 | |
| 880 | if (headerEnd.IsDone()) // Header is complete, feed it to the parser. |
| 881 | { |
| 882 | |
| 883 | #ifdef WSLC_HTTP_DEBUG |
| 884 | |
| 885 | buffer.push_back('\0'); |
| 886 | WSL_LOG( |
| 887 | "HTTPResponseDebug", TraceLoggingValue(Url.Get().c_str(), "Url"), TraceLoggingValue(buffer.data(), "Response")); |
| 888 | buffer.pop_back(); |
| 889 | |
| 890 | #endif |
| 891 | |
| 892 | boost::beast::error_code error; |
| 893 | parser.put(boost::asio::buffer(buffer.data(), buffer.size()), error); |
| 894 | |
| 895 | THROW_HR_IF_MSG( |
| 896 | E_UNEXPECTED, |
| 897 | error && error != boost::beast::http::error::need_more, |
| 898 | "Error parsing HTTP response: %hs", |
| 899 | error.what().c_str()); |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | return {parser.get(), wil::unique_socket{context->stream.release()}}; |
| 904 | } |