master
h 287 lines 12.4 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 DockerHTTPClient.h
8
9 Abstract:
10
11 This file contains the definition of the Docker HTTP client.
12
13 --*/
14
15 #pragma once
16
17 #include <boost/asio.hpp>
18 #include <boost/asio/generic/stream_protocol.hpp>
19 #include <boost/beast/core.hpp>
20 #include <boost/beast/http.hpp>
21 #include "relay.hpp"
22 #include "docker_schema.h"
23 #include "HttpHeaderEndDetector.h"
24
25 #define THROW_DOCKER_USER_ERROR_MSG(_Ex, _Msg, ...) \
26 if ((_Ex).HasErrorMessage()) \
27 { \
28 THROW_HR_WITH_USER_ERROR_MSG( \
29 (_Ex).HResultFromStatusCode(), \
30 wsl::windows::service::wslc::FormatDockerEngineError( \
31 (_Ex).DockerMessage<wsl::windows::common::docker_schema::ErrorResponse>().message), \
32 _Msg, \
33 ##__VA_ARGS__); \
34 } \
35 else \
36 { \
37 THROW_HR_MSG((_Ex).HResultFromStatusCode(), "Error: %hs. " _Msg, (_Ex).what(), ##__VA_ARGS__); \
38 }
39
40 #define CATCH_AND_THROW_DOCKER_USER_ERROR(_Msg, ...) \
41 catch (const DockerHTTPException& e) \
42 { \
43 THROW_DOCKER_USER_ERROR_MSG(e, _Msg, ##__VA_ARGS__) \
44 }
45
46 namespace wsl::windows::service::wslc {
47
48 std::string FormatDockerEngineError(const std::string& EngineMessage);
49
50 class DockerHTTPException : public std::runtime_error
51 {
52 public:
53 DockerHTTPException(
54 boost::beast::http::message<false, boost::beast::http::buffer_body>&& Response,
55 boost::beast::http::verb Method,
56 std::string&& Url,
57 std::string&& RequestContent,
58 std::string&& ResponseContent) :
59 std::runtime_error(std::format(
60 "HTTP request failed: {} {} -> {} (Request: {}, Response: {})", boost::beast::http::to_string(Method), Url, Response.result_int(), RequestContent, ResponseContent)),
61 m_response(std::move(Response)),
62 m_url(std::move(Url)),
63 m_request(std::move(RequestContent)),
64 m_responseBody(std::move(ResponseContent))
65 {
66 }
67
68 template <typename T = docker_schema::ErrorResponse>
69 T DockerMessage() const
70 {
71 return wsl::shared::FromJson<T>(m_responseBody.c_str());
72 }
73
74 // Only try to decode the error message if it's actually json.
75 bool HasErrorMessage() const
76 {
77 auto it = m_response.find(boost::beast::http::field::content_type);
78 return it != m_response.end() && it->value().starts_with("application/json");
79 }
80
81 uint16_t StatusCode() const noexcept
82 {
83 return static_cast<uint16_t>(m_response.result());
84 }
85
86 HRESULT HResultFromStatusCode() const noexcept
87 {
88 if (StatusCode() == 400)
89 {
90 return E_INVALIDARG;
91 }
92 else
93 {
94 return E_FAIL;
95 }
96 }
97
98 private:
99 boost::beast::http::message<false, boost::beast::http::buffer_body> m_response{};
100 std::string m_url;
101 std::string m_request;
102 std::string m_responseBody;
103 };
104
105 class DockerHTTPClient
106 {
107 NON_COPYABLE(DockerHTTPClient);
108
109 public:
110 using OnResponseBytes = std::function<void(gsl::span<char>)>;
111
112 struct HTTPRequestContext
113 {
114 NON_COPYABLE(HTTPRequestContext);
115 NON_MOVABLE(HTTPRequestContext);
116
117 HTTPRequestContext(wil::unique_socket&& Socket) : stream(context)
118 {
119 boost::asio::generic::stream_protocol hv_proto(AF_HYPERV, SOCK_STREAM);
120 stream.assign(hv_proto, Socket.release());
121 }
122
123 boost::asio::io_context context;
124 boost::asio::generic::stream_protocol::socket stream;
125 };
126
127 using HTTPResponse = boost::beast::http::message<false, boost::beast::http::buffer_body>;
128
129 DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE ExitingEvent, GUID VmId, ULONG ConnectTimeoutMs);
130
131 // Container management.
132 std::vector<common::docker_schema::ContainerInfo> ListContainers(
133 bool all = false, int limit = -1, const std::map<std::string, std::vector<std::string>>& filters = {});
134 common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional<std::string>& Name);
135 void StartContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
136 void StopContainer(const std::string& Id, std::optional<WSLCSignal> Signal, std::optional<LONG> TimeoutSeconds);
137 void DeleteContainer(const std::string& Id, bool Force, bool DeleteVolumes = false);
138 void SignalContainer(const std::string& Id, std::optional<WSLCSignal> Signal);
139 common::docker_schema::InspectContainer InspectContainer(const std::string& Id, bool Size = false);
140 common::docker_schema::ContainerStats ContainerStats(const std::string& Id);
141 common::docker_schema::InspectExec InspectExec(const std::string& Id);
142 wil::unique_socket AttachContainer(const std::string& Id, const std::optional<std::string>& DetachKeys);
143 void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
144 wil::unique_socket ContainerLogs(const std::string& Id, WSLCLogsFlags Flags, LONGLONG Since, LONGLONG Until, ULONGLONG Tail);
145 std::pair<uint32_t, wil::unique_socket> ExportContainer(const std::string& ContainerID);
146 std::unique_ptr<HTTPRequestContext> PutArchive(const std::string& ContainerID, const std::string& Path, std::optional<uint64_t> ContentLength);
147 std::tuple<uint32_t, wil::unique_socket, bool> GetArchive(const std::string& ContainerID, const std::string& Path);
148 common::docker_schema::PruneContainerResult PruneContainers(const std::map<std::string, std::vector<std::string>>& filters = {});
149
150 // Volume management.
151 common::docker_schema::Volume CreateVolume(const common::docker_schema::CreateVolume& Request);
152 common::docker_schema::Volume InspectVolume(const std::string& Name);
153 void RemoveVolume(const std::string& Name);
154 std::vector<common::docker_schema::Volume> ListVolumes(const std::map<std::string, std::vector<std::string>>& filters = {});
155 common::docker_schema::PruneVolumeResult PruneVolumes(const std::map<std::string, std::vector<std::string>>& filters = {});
156
157 // Network management.
158 common::docker_schema::CreateNetworkResponse CreateNetwork(const common::docker_schema::CreateNetwork& Request);
159 void RemoveNetwork(const std::string& Name);
160 std::vector<common::docker_schema::Network> ListNetworks(const std::map<std::string, std::vector<std::string>>& filters = {});
161 common::docker_schema::Network InspectNetwork(const std::string& Name);
162 void ConnectContainerToNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
163 void DisconnectContainerFromNetwork(const std::string& NetworkName, const common::docker_schema::ContainerNetworkRequest& Request);
164 common::docker_schema::PruneNetworkResult PruneNetworks(const std::map<std::string, std::vector<std::string>>& filters = {});
165
166 // Image management.
167 std::unique_ptr<HTTPRequestContext> PullImage(
168 const std::string& Repo, const std::optional<std::string>& tagOrDigest, const std::optional<std::string>& registryAuth = std::nullopt);
169 std::unique_ptr<HTTPRequestContext> ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength);
170 std::unique_ptr<HTTPRequestContext> LoadImage(uint64_t ContentLength);
171 void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag);
172 std::unique_ptr<HTTPRequestContext> PushImage(const std::string& ImageName, const std::optional<std::string>& tag, const std::string& registryAuth);
173 std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password);
174 std::vector<common::docker_schema::Image> ListImages(
175 bool all = false, bool digests = false, const std::map<std::string, std::vector<std::string>>& filters = {});
176 common::docker_schema::InspectImage InspectImage(const std::string& NameOrId);
177 std::vector<common::docker_schema::DeletedImage> DeleteImage(const char* Image, bool Force, bool NoPrune); // Image can be ID or Repo:Tag.
178 std::pair<uint32_t, wil::unique_socket> SaveImage(const std::string& NameOrId);
179 std::pair<uint32_t, wil::unique_socket> SaveImages(const std::vector<std::string>& NamesOrIds);
180 common::docker_schema::PruneImageResult PruneImages(const std::map<std::string, std::vector<std::string>>& filters = {});
181
182 // Exec.
183 common::docker_schema::CreateExecResponse CreateExec(const std::string& Container, const common::docker_schema::CreateExec& Request);
184 wil::unique_socket StartExec(const std::string& Id, const common::docker_schema::StartExec& Request);
185 void ResizeExecTty(const std::string& Id, ULONG Rows, ULONG Columns);
186
187 wil::unique_socket MonitorEvents();
188
189 struct DockerHttpResponseHandle : public common::io::ReadHandle
190 {
191 NON_COPYABLE(DockerHttpResponseHandle);
192 NON_MOVABLE(DockerHttpResponseHandle);
193
194 DockerHttpResponseHandle(
195 HTTPRequestContext& context,
196 std::function<void(const HTTPResponse&)>&& OnResponseHeader,
197 std::function<void(const gsl::span<char>&)>&& OnResponseBytes,
198 std::function<void()>&& OnCompleted = []() {});
199
200 ~DockerHttpResponseHandle();
201
202 private:
203 void OnRead(const gsl::span<char>& Content);
204 void OnResponseBytes(const gsl::span<char>& Content);
205
206 HTTPRequestContext& Context;
207 std::function<void(const boost::beast::http::message<false, boost::beast::http::buffer_body>&)> OnResponseHeader;
208 std::function<void(const gsl::span<char>&)> OnResponse;
209 std::function<void()> OnCompleted;
210 boost::beast::http::response_parser<boost::beast::http::buffer_body> Parser;
211 common::HttpHeaderEndDetector HeaderEnd;
212 std::optional<size_t> RemainingContentLength;
213 std::optional<common::io::HTTPChunkBasedReadHandle> ResponseParser;
214 };
215
216 private:
217 class URL
218 {
219 public:
220 std::string Get() const;
221 void SetParameter(std::string&& Key, std::string&& Value);
222 void SetParameter(std::string&& Key, const std::string& Value);
223 void SetParameter(std::string&& Key, const char* Value); // Overload so that pointers don't resolve to the bool method.
224 void SetParameter(std::string&& Key, bool Value);
225
226 template <typename... Args>
227 static auto Create(std::format_string<decltype(URL::Escape(std::declval<Args>()))...> Url, Args&&... args)
228 {
229 WI_ASSERT(Url.get().find_first_of("?!") == std::string::npos);
230
231 return URL(std::format(Url, Escape(std::forward<Args>(args))...));
232 }
233
234 private:
235 URL(std::string&& Path);
236
237 static std::string Escape(const std::string& Value);
238
239 std::string m_path;
240 std::multimap<std::string, std::string> m_parameters;
241 };
242
243 wil::unique_socket ConnectSocket();
244
245 std::unique_ptr<HTTPRequestContext> SendRequestImpl(
246 boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers = {});
247
248 std::pair<HTTPResponse, std::string> SendRequestAndReadResponse(
249 boost::beast::http::verb Method, const URL& Url, const std::string& Body = "");
250
251 std::pair<HTTPResponse, wil::unique_socket> SendRequest(
252 boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map<std::string, std::string>& Headers = {});
253
254 template <typename TRequest = common::docker_schema::EmptyRequest, typename TResponse = TRequest::TResponse>
255 auto Transaction(boost::beast::http::verb Method, const URL& Url, const TRequest& RequestObject = {})
256 {
257 std::string requestString;
258 if constexpr (!std::is_same_v<TRequest, common::docker_schema::EmptyRequest>)
259 {
260 requestString = wsl::shared::ToJson(RequestObject);
261 }
262
263 auto [response, body] = SendRequestAndReadResponse(Method, Url, requestString);
264
265 WSL_LOG(
266 "HTTPTransaction",
267 TraceLoggingValue(Url.Get().c_str(), "URL"),
268 TraceLoggingValue(response.result_int(), "StatusCode"));
269
270 if (response.result_int() < 200 || response.result_int() >= 300)
271 {
272 throw DockerHTTPException(std::move(response), Method, Url.Get(), std::move(requestString), std::move(body));
273 }
274
275 if constexpr (!std::is_same_v<TResponse, void>)
276 {
277 return wsl::shared::FromJson<TResponse>(body.c_str());
278 }
279 }
280
281 ULONG m_connectTimeoutMs{};
282 GUID m_vmId;
283 shared::SocketChannel m_channel;
284 HANDLE m_exitingEvent;
285 wil::srwlock m_lock;
286 };
287 } // namespace wsl::windows::service::wslc