1 | #ifndef ENGINE_SHARED_HTTP_H |
2 | #define ENGINE_SHARED_HTTP_H |
3 | |
4 | #include <base/hash_ctxt.h> |
5 | |
6 | #include <engine/shared/jobs.h> |
7 | |
8 | #include <algorithm> |
9 | #include <atomic> |
10 | #include <condition_variable> |
11 | #include <deque> |
12 | #include <mutex> |
13 | #include <optional> |
14 | #include <unordered_map> |
15 | |
16 | #include <engine/http.h> |
17 | |
18 | typedef struct _json_value json_value; |
19 | class IStorage; |
20 | |
21 | enum class EHttpState |
22 | { |
23 | ERROR = -1, |
24 | QUEUED, |
25 | RUNNING, |
26 | DONE, |
27 | ABORTED, |
28 | }; |
29 | |
30 | enum class HTTPLOG |
31 | { |
32 | NONE, |
33 | FAILURE, |
34 | ALL, |
35 | }; |
36 | |
37 | enum class IPRESOLVE |
38 | { |
39 | WHATEVER, |
40 | V4, |
41 | V6, |
42 | }; |
43 | |
44 | struct CTimeout |
45 | { |
46 | long ConnectTimeoutMs; |
47 | long TimeoutMs; |
48 | long LowSpeedLimit; |
49 | long LowSpeedTime; |
50 | }; |
51 | |
52 | class CHttpRequest : public IHttpRequest |
53 | { |
54 | friend class CHttp; |
55 | |
56 | enum class REQUEST |
57 | { |
58 | GET = 0, |
59 | HEAD, |
60 | POST, |
61 | POST_JSON, |
62 | }; |
63 | |
64 | static constexpr const char *GetRequestType(REQUEST Type) |
65 | { |
66 | switch(Type) |
67 | { |
68 | case REQUEST::GET: |
69 | return "GET" ; |
70 | case REQUEST::HEAD: |
71 | return "HEAD" ; |
72 | case REQUEST::POST: |
73 | case REQUEST::POST_JSON: |
74 | return "POST" ; |
75 | } |
76 | |
77 | // Unreachable, maybe assert instead? |
78 | return "UNKNOWN" ; |
79 | } |
80 | |
81 | char m_aUrl[256] = {0}; |
82 | |
83 | void * = nullptr; |
84 | unsigned char *m_pBody = nullptr; |
85 | size_t m_BodyLength = 0; |
86 | |
87 | CTimeout m_Timeout = CTimeout{.ConnectTimeoutMs: 0, .TimeoutMs: 0, .LowSpeedLimit: 0, .LowSpeedTime: 0}; |
88 | int64_t m_MaxResponseSize = -1; |
89 | REQUEST m_Type = REQUEST::GET; |
90 | |
91 | SHA256_DIGEST m_ActualSha256 = SHA256_ZEROED; |
92 | SHA256_CTX m_ActualSha256Ctx; |
93 | SHA256_DIGEST m_ExpectedSha256 = SHA256_ZEROED; |
94 | |
95 | bool m_WriteToFile = false; |
96 | |
97 | uint64_t m_ResponseLength = 0; |
98 | |
99 | // If `m_WriteToFile` is false. |
100 | size_t m_BufferSize = 0; |
101 | unsigned char *m_pBuffer = nullptr; |
102 | |
103 | // If `m_WriteToFile` is true. |
104 | IOHANDLE m_File = nullptr; |
105 | char m_aDestAbsolute[IO_MAX_PATH_LENGTH] = {0}; |
106 | char m_aDest[IO_MAX_PATH_LENGTH] = {0}; |
107 | |
108 | std::atomic<double> m_Size{0.0}; |
109 | std::atomic<double> m_Current{0.0}; |
110 | std::atomic<int> m_Progress{0}; |
111 | HTTPLOG m_LogProgress = HTTPLOG::ALL; |
112 | IPRESOLVE m_IpResolve = IPRESOLVE::WHATEVER; |
113 | |
114 | bool m_FailOnErrorStatus = true; |
115 | |
116 | char m_aErr[256]; // 256 == CURL_ERROR_SIZE |
117 | std::atomic<EHttpState> m_State{EHttpState::QUEUED}; |
118 | std::atomic<bool> m_Abort{false}; |
119 | |
120 | int m_StatusCode = 0; |
121 | bool = false; |
122 | std::optional<int64_t> m_ResultDate = {}; |
123 | std::optional<int64_t> m_ResultLastModified = {}; |
124 | |
125 | // Abort the request with an error if `BeforeInit()` returns false. |
126 | bool BeforeInit(); |
127 | bool ConfigureHandle(void *pHandle); // void * == CURL * |
128 | // `pHandle` can be nullptr if no handle was ever created for this request. |
129 | void OnCompletionInternal(void *pHandle, unsigned int Result); // void * == CURL *, unsigned int == CURLcode |
130 | |
131 | // Abort the request if `OnHeader()` returns something other than |
132 | // `DataSize`. `pHeader` is NOT null-terminated. |
133 | size_t (char *, size_t ); |
134 | // Abort the request if `OnData()` returns something other than |
135 | // `DataSize`. |
136 | size_t OnData(char *pData, size_t DataSize); |
137 | |
138 | static int ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr); |
139 | static size_t (char *pData, size_t Size, size_t Number, void *pUser); |
140 | static size_t WriteCallback(char *pData, size_t Size, size_t Number, void *pUser); |
141 | |
142 | protected: |
143 | // These run on the curl thread now, DO NOT STALL THE THREAD |
144 | virtual void OnProgress() {} |
145 | virtual void OnCompletion(EHttpState State) {} |
146 | |
147 | public: |
148 | CHttpRequest(const char *pUrl); |
149 | virtual ~CHttpRequest(); |
150 | |
151 | void Timeout(CTimeout Timeout) { m_Timeout = Timeout; } |
152 | void MaxResponseSize(int64_t MaxResponseSize) { m_MaxResponseSize = MaxResponseSize; } |
153 | void LogProgress(HTTPLOG LogProgress) { m_LogProgress = LogProgress; } |
154 | void IpResolve(IPRESOLVE IpResolve) { m_IpResolve = IpResolve; } |
155 | void FailOnErrorStatus(bool FailOnErrorStatus) { m_FailOnErrorStatus = FailOnErrorStatus; } |
156 | void WriteToFile(IStorage *pStorage, const char *pDest, int StorageType); |
157 | void ExpectSha256(const SHA256_DIGEST &Sha256) { m_ExpectedSha256 = Sha256; } |
158 | void Head() { m_Type = REQUEST::HEAD; } |
159 | void Post(const unsigned char *pData, size_t DataLength) |
160 | { |
161 | m_Type = REQUEST::POST; |
162 | m_BodyLength = DataLength; |
163 | m_pBody = (unsigned char *)malloc(size: std::max(a: (size_t)1, b: DataLength)); |
164 | mem_copy(dest: m_pBody, source: pData, size: DataLength); |
165 | } |
166 | void PostJson(const char *pJson) |
167 | { |
168 | m_Type = REQUEST::POST_JSON; |
169 | m_BodyLength = str_length(str: pJson); |
170 | m_pBody = (unsigned char *)malloc(size: m_BodyLength); |
171 | mem_copy(dest: m_pBody, source: pJson, size: m_BodyLength); |
172 | } |
173 | void (const char *pNameColonValue); |
174 | void (const char *pName, const char *pValue) |
175 | { |
176 | char [256]; |
177 | str_format(buffer: aHeader, buffer_size: sizeof(aHeader), format: "%s: %s" , pName, pValue); |
178 | Header(pNameColonValue: aHeader); |
179 | } |
180 | void (const char *pName, int Value) |
181 | { |
182 | char [256]; |
183 | str_format(buffer: aHeader, buffer_size: sizeof(aHeader), format: "%s: %d" , pName, Value); |
184 | Header(pNameColonValue: aHeader); |
185 | } |
186 | |
187 | const char *Dest() |
188 | { |
189 | if(m_WriteToFile) |
190 | { |
191 | return m_aDest; |
192 | } |
193 | else |
194 | { |
195 | return nullptr; |
196 | } |
197 | } |
198 | |
199 | double Current() const { return m_Current.load(m: std::memory_order_relaxed); } |
200 | double Size() const { return m_Size.load(m: std::memory_order_relaxed); } |
201 | int Progress() const { return m_Progress.load(m: std::memory_order_relaxed); } |
202 | EHttpState State() const { return m_State; } |
203 | bool Done() const |
204 | { |
205 | EHttpState State = m_State; |
206 | return State != EHttpState::QUEUED && State != EHttpState::RUNNING; |
207 | } |
208 | void Abort() { m_Abort = true; } |
209 | |
210 | void Wait(); |
211 | |
212 | void Result(unsigned char **ppResult, size_t *pResultLength) const; |
213 | json_value *ResultJson() const; |
214 | const SHA256_DIGEST &ResultSha256() const; |
215 | |
216 | int StatusCode() const; |
217 | std::optional<int64_t> ResultAgeSeconds() const; |
218 | std::optional<int64_t> ResultLastModified() const; |
219 | }; |
220 | |
221 | inline std::unique_ptr<CHttpRequest> HttpHead(const char *pUrl) |
222 | { |
223 | auto pResult = std::make_unique<CHttpRequest>(args&: pUrl); |
224 | pResult->Head(); |
225 | return pResult; |
226 | } |
227 | |
228 | inline std::unique_ptr<CHttpRequest> HttpGet(const char *pUrl) |
229 | { |
230 | return std::make_unique<CHttpRequest>(args&: pUrl); |
231 | } |
232 | |
233 | inline std::unique_ptr<CHttpRequest> HttpGetFile(const char *pUrl, IStorage *pStorage, const char *pOutputFile, int StorageType) |
234 | { |
235 | std::unique_ptr<CHttpRequest> pResult = HttpGet(pUrl); |
236 | pResult->WriteToFile(pStorage, pDest: pOutputFile, StorageType); |
237 | pResult->Timeout(Timeout: CTimeout{.ConnectTimeoutMs: 4000, .TimeoutMs: 0, .LowSpeedLimit: 500, .LowSpeedTime: 5}); |
238 | return pResult; |
239 | } |
240 | |
241 | inline std::unique_ptr<CHttpRequest> HttpPost(const char *pUrl, const unsigned char *pData, size_t DataLength) |
242 | { |
243 | auto pResult = std::make_unique<CHttpRequest>(args&: pUrl); |
244 | pResult->Post(pData, DataLength); |
245 | pResult->Timeout(Timeout: CTimeout{.ConnectTimeoutMs: 4000, .TimeoutMs: 15000, .LowSpeedLimit: 500, .LowSpeedTime: 5}); |
246 | return pResult; |
247 | } |
248 | |
249 | inline std::unique_ptr<CHttpRequest> HttpPostJson(const char *pUrl, const char *pJson) |
250 | { |
251 | auto pResult = std::make_unique<CHttpRequest>(args&: pUrl); |
252 | pResult->PostJson(pJson); |
253 | pResult->Timeout(Timeout: CTimeout{.ConnectTimeoutMs: 4000, .TimeoutMs: 15000, .LowSpeedLimit: 500, .LowSpeedTime: 5}); |
254 | return pResult; |
255 | } |
256 | |
257 | void EscapeUrl(char *pBuf, int Size, const char *pStr); |
258 | bool HttpHasIpresolveBug(); |
259 | |
260 | // In an ideal world this would be a kernel interface |
261 | class CHttp : public IHttp |
262 | { |
263 | enum EState |
264 | { |
265 | UNINITIALIZED, |
266 | RUNNING, |
267 | ERROR, |
268 | }; |
269 | |
270 | void *m_pThread = nullptr; |
271 | |
272 | std::mutex m_Lock{}; |
273 | std::condition_variable m_Cv{}; |
274 | std::atomic<EState> m_State = UNINITIALIZED; |
275 | std::deque<std::shared_ptr<CHttpRequest>> m_PendingRequests{}; |
276 | std::unordered_map<void *, std::shared_ptr<CHttpRequest>> m_RunningRequests{}; // void * == CURL * |
277 | std::chrono::milliseconds m_ShutdownDelay{}; |
278 | std::optional<std::chrono::time_point<std::chrono::steady_clock>> m_ShutdownTime{}; |
279 | std::atomic<bool> m_Shutdown = false; |
280 | |
281 | // Only to be used with curl_multi_wakeup |
282 | void *m_pMultiH = nullptr; // void * == CURLM * |
283 | |
284 | static void ThreadMain(void *pUser); |
285 | void RunLoop(); |
286 | |
287 | public: |
288 | // Startup |
289 | bool Init(std::chrono::milliseconds ShutdownDelay); |
290 | |
291 | // User |
292 | virtual void Run(std::shared_ptr<IHttpRequest> pRequest) override; |
293 | void Shutdown() override; |
294 | ~CHttp(); |
295 | }; |
296 | |
297 | #endif // ENGINE_SHARED_HTTP_H |
298 | |