master
cpp 904 lines 29.8 KB
Raw
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 }