products_log 1.0.0
C++ client library for the Products REST API (usage statistics)
Loading...
Searching...
No Matches
products_log.cpp
Go to the documentation of this file.
1/// \file products_log.cpp
2/// \brief Реализация клиента ProductsLog.
3
5
6#include <cctype>
7#include <cstdio>
8#include <cstdlib>
9#include <sstream>
10
11#include <curl/curl.h>
12
13namespace products_log {
14
15namespace {
16
17std::string GetEnv(const char *name) {
18 if (const char *v = std::getenv(name))
19 return v;
20 return "";
21}
22
23// Предупреждение идёт в stderr, как у остальных логов ebpf-blocker.
24void Warn(const std::string &msg) {
25 std::fprintf(stderr, "ProductsLog: %s\n", msg.c_str());
26}
27
28// Убирает завершающий слэш, как в Python-версии (rstrip("/")).
29std::string StripTrailingSlash(std::string s) {
30 while (!s.empty() && s.back() == '/')
31 s.pop_back();
32 return s;
33}
34
36 std::string data;
37};
38
39size_t CurlWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata) {
40 auto *buf = static_cast<CurlWriteBuffer *>(userdata);
41 buf->data.append(ptr, size * nmemb);
42 return size * nmemb;
43}
44
46 HttpResponse resp;
47
48 CURL *curl = curl_easy_init();
49 if (!curl) {
50 resp.error = "curl_easy_init failed";
51 return resp;
52 }
53
54 curl_slist *headers = nullptr;
55 if (!req.body.empty())
56 headers = curl_slist_append(headers, "Content-Type: application/json");
57
59
60 curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
61 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback);
62 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
63 curl_easy_setopt(curl, CURLOPT_TIMEOUT,
64 static_cast<long>(req.timeout_seconds));
65 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT,
66 static_cast<long>(req.timeout_seconds));
67 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
68 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
69 curl_easy_setopt(curl, CURLOPT_USERAGENT, "products_log/1.0");
70
71 if (!req.username.empty() || !req.password.empty()) {
72 const std::string userpwd = req.username + ":" + req.password;
73 curl_easy_setopt(curl, CURLOPT_USERPWD, userpwd.c_str());
74 }
75
76 if (req.method == "POST") {
77 curl_easy_setopt(curl, CURLOPT_POST, 1L);
78 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.c_str());
79 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE,
80 static_cast<long>(req.body.size()));
81 }
82
83 const CURLcode rc = curl_easy_perform(curl);
84 if (rc != CURLE_OK) {
85 resp.error = curl_easy_strerror(rc);
86 } else {
87 resp.ok = true;
88 long http_code = 0;
89 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
90 resp.status = static_cast<int>(http_code);
91 }
92
93 resp.body = buf.data;
94 curl_slist_free_all(headers);
95 curl_easy_cleanup(curl);
96 return resp;
97}
98
99} // namespace
100
101ProductsLog::ProductsLog(std::string product_name, std::string product_version,
102 Transport transport)
103 : ProductsLog(std::move(product_name), std::move(product_version),
104 GetEnv("PRODUCTS_HOST"), GetEnv("PRODUCTS_USER"),
105 GetEnv("PRODUCTS_PASS"), std::move(transport)) {}
106
107ProductsLog::ProductsLog(std::string product_name, std::string product_version,
108 std::string base_url, std::string username,
109 std::string password, Transport transport)
110 : product_name_(std::move(product_name)),
111 product_version_(product_version.empty() ? "UNKNOWN"
112 : std::move(product_version)),
113 base_url_(StripTrailingSlash(std::move(base_url))),
114 username_(std::move(username)), password_(std::move(password)),
115 transport_(std::move(transport)) {
116 enabled_ = !base_url_.empty() && !username_.empty() && !password_.empty();
117 if (!enabled_) {
118 Warn("PRODUCTS_HOST / PRODUCTS_USER / PRODUCTS_PASS are not all set — "
119 "statistics reporting is disabled.");
120 }
121}
122
124 static const Transport impl = [](const HttpRequest &req) {
125 return CurlTransportImpl(req);
126 };
127 return impl;
128}
129
130std::string ProductsLog::UrlEncode(const std::string &s) {
131 static const char *hex = "0123456789ABCDEF";
132 std::string out;
133 out.reserve(s.size());
134 for (unsigned char c : s) {
135 if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
136 out.push_back(static_cast<char>(c));
137 } else {
138 out.push_back('%');
139 out.push_back(hex[c >> 4]);
140 out.push_back(hex[c & 0x0F]);
141 }
142 }
143 return out;
144}
145
146std::string ProductsLog::JsonEscape(const std::string &s) {
147 std::ostringstream out;
148 for (unsigned char c : s) {
149 switch (c) {
150 case '"':
151 out << "\\\"";
152 break;
153 case '\\':
154 out << "\\\\";
155 break;
156 case '\n':
157 out << "\\n";
158 break;
159 case '\r':
160 out << "\\r";
161 break;
162 case '\t':
163 out << "\\t";
164 break;
165 case '\b':
166 out << "\\b";
167 break;
168 case '\f':
169 out << "\\f";
170 break;
171 default:
172 if (c < 0x20) {
173 static const char *hex = "0123456789abcdef";
174 out << "\\u00" << hex[c >> 4] << hex[c & 0x0F];
175 } else {
176 out << static_cast<char>(c);
177 }
178 }
179 }
180 return out.str();
181}
182
183std::optional<int64_t> ProductsLog::ParseProductId(const std::string &body) {
184 // Ищем ключ "id" и число после него: {"id": 42, ...} либо {"id":42}.
185 const std::string marker = "\"id\"";
186 std::size_t pos = body.find(marker);
187 if (pos == std::string::npos)
188 return std::nullopt;
189
190 pos = body.find(':', pos + marker.size());
191 if (pos == std::string::npos)
192 return std::nullopt;
193 ++pos;
194
195 while (pos < body.size() &&
196 std::isspace(static_cast<unsigned char>(body[pos])))
197 ++pos;
198
199 int64_t value = 0;
200 bool negative = false;
201 bool any = false;
202 if (pos < body.size() && body[pos] == '-') {
203 negative = true;
204 ++pos;
205 }
206 while (pos < body.size() &&
207 std::isdigit(static_cast<unsigned char>(body[pos]))) {
208 value = value * 10 + (body[pos] - '0');
209 any = true;
210 ++pos;
211 }
212 if (!any)
213 return std::nullopt;
214 return negative ? -value : value;
215}
216
218 if (product_id_.has_value())
219 return true;
220
221 if (auto id = FindProductId()) {
222 product_id_ = id;
223 return true;
224 }
225 if (auto id = RegisterProduct()) {
226 product_id_ = id;
227 return true;
228 }
229 return false;
230}
231
232bool ProductsLog::SendStat(int64_t product_id, const std::string &comment) {
233 const std::string url = base_url_ + "/stats";
234 std::ostringstream body;
235 body << "{\"product\":{\"id\": " << product_id << "}," << "\"version\":\""
236 << JsonEscape(product_version_) << "\", " << "\"comment\": \""
237 << JsonEscape(comment) << "\"}";
238
239 HttpRequest req{};
240 req.method = "POST";
241 req.url = url;
242 req.body = body.str();
243 req.username = username_;
244 req.password = password_;
245
246 HttpResponse resp;
247 try {
248 resp = transport_(req);
249 } catch (...) {
250 Warn("post_message failed: transport threw an exception");
251 return false;
252 }
253
254 if (!resp.ok) {
255 Warn("post_message failed: " +
256 (resp.error.empty() ? "transport error" : resp.error));
257 return false;
258 }
259 if (resp.status < 200 || resp.status >= 300) {
260 std::ostringstream msg;
261 msg << "post_message failed (HTTP " << resp.status << ")";
262 Warn(msg.str());
263 return false;
264 }
265 return true;
266}
267
268bool ProductsLog::PostMessage(const std::string &comment) {
269 if (!enabled_ || !ResolveProductId())
270 return false;
271 return SendStat(*product_id_, comment);
272}
273
274bool ProductsLog::PostStart() { return PostMessage("Program started"); }
275
276bool ProductsLog::PostShutdown() { return PostMessage("Program shutdown"); }
277
278std::optional<int64_t> ProductsLog::FindProductId() {
279 const std::string url =
280 base_url_ + "/products/byName/" + UrlEncode(product_name_);
281 HttpRequest req{};
282 req.method = "GET";
283 req.url = url;
284 req.username = username_;
285 req.password = password_;
286
287 HttpResponse resp;
288 try {
289 resp = transport_(req);
290 } catch (...) {
291 Warn("failed to look up product: transport threw an exception");
292 return std::nullopt;
293 }
294
295 if (!resp.ok) {
296 Warn("failed to look up product: " +
297 (resp.error.empty() ? "transport error" : resp.error));
298 return std::nullopt;
299 }
300 if (resp.status != 200)
301 return std::nullopt;
302 return ParseProductId(resp.body);
303}
304
305std::optional<int64_t> ProductsLog::RegisterProduct() {
306 const std::string url = base_url_ + "/products";
307 HttpRequest req{};
308 req.method = "POST";
309 req.url = url;
310 req.body = "{\"name\":\"" + JsonEscape(product_name_) + "\"}";
311 req.username = username_;
312 req.password = password_;
313
314 HttpResponse resp;
315 try {
316 resp = transport_(req);
317 } catch (...) {
318 Warn("failed to register product: transport threw an exception");
319 return std::nullopt;
320 }
321
322 if (!resp.ok) {
323 Warn("failed to register product: " +
324 (resp.error.empty() ? "transport error" : resp.error));
325 return std::nullopt;
326 }
327 if (resp.status != 200 && resp.status != 201) {
328 std::ostringstream msg;
329 msg << "failed to register product (HTTP " << resp.status << ")";
330 Warn(msg.str());
331 return std::nullopt;
332 }
333 return ParseProductId(resp.body);
334}
335
336} // namespace products_log
bool PostShutdown()
Отправить "Program shutdown".
bool PostStart()
Отправить "Program started".
ProductsLog(std::string product_name, std::string product_version, Transport transport=DefaultTransport())
bool PostMessage(const std::string &comment)
Отправить запись статистики. Возвращает false при любой ошибке.
static std::string JsonEscape(const std::string &s)
static std::string UrlEncode(const std::string &s)
bool SendStat(int64_t product_id, const std::string &comment)
std::optional< int64_t > FindProductId()
static std::optional< int64_t > ParseProductId(const std::string &body)
static Transport DefaultTransport()
std::optional< int64_t > RegisterProduct()
std::optional< int64_t > product_id_
size_t CurlWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata)
HttpResponse CurlTransportImpl(const HttpRequest &req)
std::function< HttpResponse(const HttpRequest &)> Transport
Транспорт HTTP — функция, выполняющая запрос. Заменяемая для тестов.
Клиентская библиотека для REST API Products (статистика использования приложений).
HTTP-запрос, описывающий обращение к Products API.
std::string method
"GET" или "POST".
int timeout_seconds
Таймаут запроса в секундах.
std::string username
Basic-auth пользователь (пусто = без авторизации).
std::string body
Тело запроса (для POST).
std::string url
Полный URL (путь уже URL-закодирован).
std::string password
Basic-auth пароль.
Ответ на запрос.
std::string error
Описание ошибки (без секретов).
std::string body
Тело ответа.
bool ok
true, если транспорт донёс запрос до сервера.
int status
HTTP-код статуса (0 при транспортной ошибке).