File: curlEngine.d

package info (click to toggle)
onedrive 2.5.10-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 13,252 kB
  • sloc: sh: 660; makefile: 167
file content (798 lines) | stat: -rw-r--r-- 29,805 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
// What is this module called?
module curlEngine;

// What does this module require to function?
import std.net.curl;
import etc.c.curl;
import std.datetime;
import std.conv;
import std.file;
import std.format;
import std.json;
import std.stdio;
import std.range;
import core.memory;
import core.sys.posix.signal;
// Required for WebSocket Support
import core.stdc.stdlib : getenv;
import core.stdc.string : strcmp;
import core.sys.posix.dlfcn : dlopen, dlsym, dlclose, RTLD_NOW; // Posix elements
import std.exception : enforce;     // for enforce(...)

// What other modules that we have created do we need to import?
import log;
import util;

// WebSocket check elements
enum CURL_WS_MIN_NUM = 0x075600; // 7.86.0 (version which WebSocket support was added to cURL)

extern (C) void sigpipeHandler(int signum) {
	// Custom handler to ignore SIGPIPE signals
	addLogEntry("ERROR: Handling a cURL SIGPIPE signal despite CURLOPT_NOSIGNAL being set (cURL Operational Bug) ...");
}

// Function pointer types matching libcurl WebSocket (WS) API
extern(C) struct curl_ws_frame {
	uint age;
	uint flags;
	size_t len;
	size_t offset;
	size_t bytesleft;
}

// WebSocket alias
alias PFN_curl_ws_recv =
	extern(C) CURLcode function(CURL*, void*, size_t, size_t*, const curl_ws_frame**);
alias PFN_curl_ws_send =
	extern(C) CURLcode function(CURL*, const void*, size_t, size_t*, long /*curl_off_t*/, uint);

extern(C) struct curl_slist { char* data; curl_slist* next; }
extern(C) curl_slist* curl_slist_append(curl_slist* list, const char* string);
extern(C) void curl_slist_free_all(curl_slist* list);

// Shared pool of CurlEngine instances accessible across all threads
__gshared CurlEngine[] curlEnginePool; // __gshared is used to declare a variable that is shared across all threads

private __gshared {
	void*                 _curlLib;
	PFN_curl_ws_recv      p_curl_ws_recv;
	PFN_curl_ws_send      p_curl_ws_send;
	bool                  _wsSymbolsReady;
	uint                  _wsProbeOnce; // 0=not run, 1=success, 2=fail
}

private void* loadCurlLib() {
	// Respect LD_LIBRARY_PATH etc.
	auto h = dlopen("libcurl.so.4", RTLD_NOW);
	if (h is null) h = dlopen("libcurl.so", RTLD_NOW);
	return h;
}

private void* findSymbol(const(char)* name) {
	return dlsym(_curlLib, name);
}

private bool probeCurlWsSymbols() {
	if (_wsProbeOnce == 1) return _wsSymbolsReady;
	if (_wsProbeOnce == 2) return false;

	// 1) libcurl version check
	auto vi = curl_version_info(CURLVERSION_NOW);
	if (vi is null || vi.version_num < CURL_WS_MIN_NUM) {
		_wsProbeOnce = 2; _wsSymbolsReady = false; return false;
	}

	// 2) load libcurl and resolve symbols
	_curlLib = loadCurlLib();
	if (_curlLib is null) {
		_wsProbeOnce = 2; _wsSymbolsReady = false; return false;
	}

	p_curl_ws_recv = cast(PFN_curl_ws_recv) findSymbol("curl_ws_recv");
	p_curl_ws_send = cast(PFN_curl_ws_send) findSymbol("curl_ws_send");

	_wsSymbolsReady = (p_curl_ws_recv !is null) && (p_curl_ws_send !is null);
	_wsProbeOnce = _wsSymbolsReady ? 1 : 2;
	return _wsSymbolsReady;
}

bool curlSupportsWebSockets() {
	return probeCurlWsSymbols();
}

class CurlResponse {
	HTTP.Method method;
	const(char)[] url;
	const(char)[][const(char)[]] requestHeaders;
	const(char)[] postBody;

	bool hasResponse;
	string[string] responseHeaders;
	HTTP.StatusLine statusLine;
	char[] content;

	this() {
		reset();
	}
	
	~this() {
		reset();
	}

	void reset() {
		method = HTTP.Method.undefined;
		url = "";
		requestHeaders = null;
		postBody = [];
		hasResponse = false;
		responseHeaders = null;
		statusLine.reset();
		content = [];
	}

	void addRequestHeader(const(char)[] name, const(char)[] value) {
		requestHeaders[to!string(name)] = to!string(value);
	}

	void connect(HTTP.Method method, const(char)[] url) {
		this.method = method;
		this.url = url;
	}

	const JSONValue json() {
		JSONValue json;
		try {
			json = content.parseJSON();
		} catch (JSONException e) {
			// Log that a JSON Exception was caught, dont output the HTML response from OneDrive
			if (debugLogging) {addLogEntry("JSON Exception caught when performing HTTP operations - use --debug-https to diagnose further", ["debug"]);}
		}
		return json;
	};

	void update(HTTP *http) {
		hasResponse = true;
		this.responseHeaders = http.responseHeaders();
		this.statusLine = http.statusLine;
		
		// has 'microsoftDataCentre' been set yet?
		if (microsoftDataCentre.empty) {
			// Extract the 'x-ms-ags-diagnostic' header if it exists
			if ("x-ms-ags-diagnostic" in this.responseHeaders) {
				// try and extract the data centre details
				try {
					// attempt to extract the data centre location from the header
					auto diagHeaderData = parseJSON(this.responseHeaders["x-ms-ags-diagnostic"]);
					string dataCentre = diagHeaderData["ServerInfo"]["DataCenter"].str;
					// set the Microsoft Data Centre value
					microsoftDataCentre = dataCentre;
				} catch (Exception e) {
					// do nothing
				}	
			}
		}
				
		// Output the response headers only if using debug mode + debugging https itself
		if ((debugLogging) && (debugHTTPSResponse)) {
			addLogEntry("HTTP Response Headers: " ~ to!string(this.responseHeaders), ["debug"]);
			addLogEntry("HTTP Status Line: " ~ to!string(this.statusLine), ["debug"]);
		}
	}

	@safe pure HTTP.StatusLine getStatus() {
		return this.statusLine;
	}

	// Return the current value of retryAfterValue
	int getRetryAfterValue() {
		int delayBeforeRetry;
		// Is 'retry-after' in the response headers
		if ("retry-after" in responseHeaders) {
			// Set the retry-after value
			if (debugLogging) {
				addLogEntry("curlEngine.http.perform() => Received a 'Retry-After' Header Response with the following value: " ~ to!string(responseHeaders["retry-after"]), ["debug"]);
				addLogEntry("curlEngine.http.perform() => Setting retryAfterValue to: " ~ responseHeaders["retry-after"], ["debug"]);
			}
			delayBeforeRetry = to!int(responseHeaders["retry-after"]);
		} else {
			// Use a 120 second delay as a default given header value was zero
			// This value is based on log files and data when determining correct process for 429 response handling
			delayBeforeRetry = 120;
			// Update that we are over-riding the provided value with a default
			if (debugLogging) {addLogEntry("HTTP Response Header retry-after value was missing - Using a preconfigured default of: " ~ to!string(delayBeforeRetry), ["debug"]);}
		}
		return delayBeforeRetry;
	}
	
	const string parseRequestHeaders(const(const(char)[][const(char)[]]) headers) {
		string requestHeadersStr = "";
		// Ensure response headers is not null and iterate over keys safely.
		if (headers !is null) {
			foreach (string header; headers.byKey()) {
				if (header == "Authorization") {
					continue;
				}
				// Use the 'in' operator to safely check if the key exists in the associative array.
				if (auto val = header in headers) {
					requestHeadersStr ~= "< " ~ header ~ ": " ~ *val ~ "\n";
				}
			}
		}
		return requestHeadersStr;
	}

	const string parseResponseHeaders(const(string[string]) headers) {
		string responseHeadersStr = "";
		// Ensure response headers is not null and iterate over keys safely.
		if (headers !is null) {
			foreach (string header; headers.byKey()) {
				// Check if the key actually exists before accessing it to avoid RangeError.
				if (auto val = header in headers) { // 'in' checks for the key and returns a pointer to the value if found.
					responseHeadersStr ~= "> " ~ header ~ ": " ~ *val ~ "\n"; // Dereference pointer to get the value.
				}
			}
		}
		return responseHeadersStr;
	}

	const string dumpDebug() {
		import std.range;
		import std.format : format;
		
		string str = "";
		str ~= format("< %s %s\n", method, url);
		if (!requestHeaders.empty) {
			str ~= parseRequestHeaders(requestHeaders);
		}
		if (!postBody.empty) {
			str ~= format("\n----\n%s\n----\n", postBody);
		}
		str ~= format("< %s\n", statusLine);
		if (!responseHeaders.empty) {
			str ~= parseResponseHeaders(responseHeaders);
		}
		return str;
	}

	const string dumpResponse() {
		import std.range;
		import std.format : format;

		string str = "";
		if (!content.empty) {
			str ~= format("\n----\n%s\n----\n", content);
		}
		return str;
	}

	override string toString() const {
		string str = "Curl debugging: \n";
		str ~= dumpDebug();
		if (hasResponse) {
			str ~= "Curl response: \n";
			str ~= dumpResponse();
		}
		return str;
	}
}

class CurlEngine {

	HTTP http;
	File uploadFile;
	CurlResponse response;
	bool keepAlive;
	ulong dnsTimeout;
	string internalThreadId;
	SysTime releaseTimestamp;
	ulong maxIdleTime;
	private long resumeFromOffset = -1;
	
    this() {
        http = HTTP();   // Directly initializes HTTP using its default constructor
        response = null; // Initialize as null
		internalThreadId = generateAlphanumericString(); // Give this CurlEngine instance a unique ID
		if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Created new CurlEngine instance id: " ~ to!string(internalThreadId), ["debug"]);}
    }

	// The destructor should only clean up resources owned directly by this CurlEngine instance
	~this() {
		// Is the file still open?
		if (uploadFile.isOpen()) {
			uploadFile.close();
		}
		// Is 'response' cleared?
		object.destroy(response); // Destroy, then set to null
		response = null;
		// Is the actual http instance is stopped?
		if (!http.isStopped) {
			http.shutdown();
		}
		// Make sure this HTTP instance is destroyed
		object.destroy(http);
		// ThreadId needs to be set to null
		internalThreadId = null;
    }
		
	// We are releasing a curl instance back to the pool
	void releaseEngine() {
		// Set timestamp of release
		releaseTimestamp = Clock.currTime(UTC());
		// Log that we are releasing this engine back to the pool
		if ((debugLogging) && (debugHTTPSResponse)) {
			addLogEntry("CurlEngine releaseEngine() called on instance id: " ~ to!string(internalThreadId), ["debug"]);
			addLogEntry("CurlEngine curlEnginePool size before release: " ~ to!string(curlEnginePool.length), ["debug"]);
			string engineReleaseMessage = format("Release Timestamp for CurlEngine %s: %s", to!string(internalThreadId), to!string(releaseTimestamp));
			addLogEntry(engineReleaseMessage, ["debug"]);
		}
		
		// cleanup this curl instance before putting it back in the pool
		cleanup(true); // Cleanup instance by resetting values and flushing cookie cache
        synchronized (CurlEngine.classinfo) {
            curlEnginePool ~= this;
			if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool size after release: " ~ to!string(curlEnginePool.length), ["debug"]);}
        }
		// Perform Garbage Collection
		GC.collect();
		// Return free memory to the OS
		GC.minimize();
    }
	
	// Setup a specific SIGPIPE Signal handler due to curl bugs that ignore CurlOption.nosignal
	void setupSIGPIPESignalHandler() {
		// Setup the signal handler
		sigaction_t curlAction;
		curlAction.sa_handler = &sigpipeHandler; // Direct function pointer assignment
		sigaction(SIGPIPE, &curlAction, null); // Broken Pipe signal from curl
	}
	
	// Initialise this curl instance
	void initialise(ulong dnsTimeout, ulong connectTimeout, ulong dataTimeout, ulong operationTimeout, int maxRedirects, bool httpsDebug, string userAgent, bool httpProtocol, ulong userRateLimit, ulong protocolVersion, ulong maxIdleTime, bool keepAlive=true) {
		// There are many broken curl versions being used, mainly provided by Ubuntu
		// Ignore SIGPIPE to prevent the application from exiting without reason with an exit code of 141 when bad curl version generate this signal despite being told not to (CurlOption.nosignal) below
		setupSIGPIPESignalHandler();
		
		// Setting 'keepAlive' to false ensures that when we close the curl instance, any open sockets are closed - which we need to do when running 
		// multiple threads and API instances at the same time otherwise we run out of local files | sockets pretty quickly
		this.keepAlive = keepAlive;
		
		// Curl DNS Timeout Handling
		this.dnsTimeout = dnsTimeout;

		// Curl Timeout Handling
		this.maxIdleTime = maxIdleTime;
		
		// libcurl dns_cache_timeout timeout
		// https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html
		// https://dlang.org/library/std/net/curl/http.dns_timeout.html
		http.dnsTimeout = (dur!"seconds"(dnsTimeout));
		
		// Timeout for HTTPS connections
		// https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
		// https://dlang.org/library/std/net/curl/http.connect_timeout.html
		http.connectTimeout = (dur!"seconds"(connectTimeout));
		
		// Timeout for activity on connection
		// This is a DMD | DLANG specific item, not a libcurl item
		// https://dlang.org/library/std/net/curl/http.data_timeout.html
		// https://raw.githubusercontent.com/dlang/phobos/master/std/net/curl.d - private enum _defaultDataTimeout = dur!"minutes"(2);
		http.dataTimeout = (dur!"seconds"(dataTimeout));
		
		// Maximum time any operation is allowed to take
		// This includes dns resolution, connecting, data transfer, etc.
		// https://curl.se/libcurl/c/CURLOPT_TIMEOUT_MS.html
		// https://dlang.org/library/std/net/curl/http.operation_timeout.html
		http.operationTimeout = (dur!"seconds"(operationTimeout));
		
		// Specify how many redirects should be allowed
		http.maxRedirects(maxRedirects);
		// Debug HTTPS
		http.verbose = httpsDebug;
		// Use the configured 'user_agent' value
		http.setUserAgent = userAgent;
		// What IP protocol version should be used when using Curl - IPv4 & IPv6, IPv4 or IPv6
		http.handle.set(CurlOption.ipresolve,protocolVersion); // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only
		
		// What version of HTTP protocol do we use?
		// Curl >= 7.62.0 defaults to http2 for a significant number of operations
		if (httpProtocol) {
			// Downgrade to HTTP 1.1 - yes version = 2 is HTTP 1.1
			http.handle.set(CurlOption.http_version,2);
		}
		
		// Configure upload / download rate limits if configured
		// 131072 = 128 KB/s - minimum for basic application operations to prevent timeouts
		// A 0 value means rate is unlimited, and is the curl default
		if (userRateLimit > 0) {
			// set rate limit
			http.handle.set(CurlOption.max_send_speed_large,userRateLimit);
			http.handle.set(CurlOption.max_recv_speed_large,userRateLimit);
		}
		
		// Explicitly set libcurl options to avoid using signal handlers in a multi-threaded environment
		// See: https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html
		// The CURLOPT_NOSIGNAL option is intended for use in multi-threaded programs to ensure that libcurl does not use any signal handling.
		// Set CURLOPT_NOSIGNAL to 1 to prevent libcurl from using signal handlers, thus avoiding interference with the application's signal handling which could lead to issues such as unstable behavior or application crashes.
		http.handle.set(CurlOption.nosignal,1);
		
		//   https://curl.se/libcurl/c/CURLOPT_TCP_NODELAY.html
		//   Ensure that TCP_NODELAY is set to 0 to ensure that TCP NAGLE is enabled
		http.handle.set(CurlOption.tcp_nodelay,0);
		
		//   https://curl.se/libcurl/c/CURLOPT_FORBID_REUSE.html
		//   CURLOPT_FORBID_REUSE - make connection get closed at once after use
		//   Setting this to 0 ensures that we ARE reusing connections (we did this in v2.4.xx) to ensure connections remained open and usable
		//   Setting this to 1 ensures that when we close the curl instance, any open sockets are forced closed when the API curl instance is destroyed
		//   The libcurl default is 0 as per the documentation (to REUSE connections) - ensure we are configuring to reuse sockets
		http.handle.set(CurlOption.forbid_reuse,0);
		
		if (httpsDebug) {
			// Output what options we are using so that in the debug log this can be tracked
			if ((debugLogging) && (debugHTTPSResponse)) {
				addLogEntry("http.dnsTimeout = " ~ to!string(dnsTimeout), ["debug"]);
				addLogEntry("http.connectTimeout = " ~ to!string(connectTimeout), ["debug"]);
				addLogEntry("http.dataTimeout = " ~ to!string(dataTimeout), ["debug"]);
				addLogEntry("http.operationTimeout = " ~ to!string(operationTimeout), ["debug"]);
				addLogEntry("http.maxRedirects = " ~ to!string(maxRedirects), ["debug"]);
				addLogEntry("http.CurlOption.ipresolve = " ~ to!string(protocolVersion), ["debug"]);
				addLogEntry("http.header.Connection.keepAlive = " ~ to!string(keepAlive), ["debug"]);
			}
		}
	}

	void setResponseHolder(CurlResponse response) {
		if (response is null) {
			// Create a response instance if it doesn't already exist
			if (this.response is null)
				this.response = new CurlResponse();
		} else {
			this.response = response;
		}
	}

	void addRequestHeader(const(char)[] name, const(char)[] value) {
		setResponseHolder(null);
		http.addRequestHeader(name, value);
		response.addRequestHeader(name, value);
	}

	void connect(HTTP.Method method, const(char)[] url) {
		setResponseHolder(null);
		if (!keepAlive)
			addRequestHeader("Connection", "close");
		http.method = method;
		http.url = url;
		response.connect(method, url);
	}

	void setContent(const(char)[] contentType, const(char)[] sendData) {
		setResponseHolder(null);
		addRequestHeader("Content-Type", contentType);
		if (sendData) {
			http.contentLength = sendData.length;
			http.onSend = (void[] buf) {
				import std.algorithm: min;
				size_t minLen = min(buf.length, sendData.length);
				if (minLen == 0) return 0;
				buf[0 .. minLen] = cast(void[]) sendData[0 .. minLen];
				sendData = sendData[minLen .. $];
				return minLen;
			};
			response.postBody = sendData;
		}
	}

	void setFile(string filepath, string contentRange, ulong offset, ulong offsetSize) {
		setResponseHolder(null);
		// open file as read-only in binary mode
		uploadFile = File(filepath, "rb");

		if (contentRange.empty) {
			offsetSize = uploadFile.size();
		} else {
			addRequestHeader("Content-Range", contentRange);
			uploadFile.seek(offset);
		}

		// Setup progress bar to display
		http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {
			return 0;
		};
		
		addRequestHeader("Content-Type", "application/octet-stream");
		http.onSend = data => uploadFile.rawRead(data).length;
		http.contentLength = offsetSize;
	}
	
	void setZeroContentLength() {
		// Explicit HTTP semantics
		http.contentLength = 0;
		addRequestHeader("Content-Length", to!string(0));
		
		// Force libcurl POST-with-empty-body semantics
		// This prevents libcurl from attempting to read from stdin when performing a POST with no payload.
		http.handle.set(CurlOption.postfields, "");
		http.handle.set(CurlOption.postfieldsize, 0L);
		
		// Defensive: ensure we are NOT in upload/read-callback mode
		http.handle.set(CurlOption.upload, 0);
	}

	CurlResponse execute() {
		scope(exit) {
			cleanup();
		}
		setResponseHolder(null);
		http.onReceive = (ubyte[] data) {
			response.content ~= data;
			// HTTP Server Response Code Debugging if --https-debug is being used
			return data.length;
		};
		http.perform();
		response.update(&http);
		return response;
	}

	CurlResponse download(string originalFilename, string downloadFilename) {
		setResponseHolder(null);
		
		// Open the file in append mode if resuming, else write mode
		auto file = (resumeFromOffset > 0)
			? File(downloadFilename, "ab") // append binary
			: File(downloadFilename, "wb"); // write binary

		// Function exit scope
		scope(exit) {
			cleanup();
			if (file.isOpen()){
				// close open file
				file.close();
			}
		}
		
		// Apply Range header if resuming
		if (resumeFromOffset > 0) {
			string rangeHeader = format("bytes=%d-", resumeFromOffset);
			addRequestHeader("Range", rangeHeader);
		}
		
		// Receive data
		http.onReceive = (ubyte[] data) {
			file.rawWrite(data);
			return data.length;
		};
		
		// Perform HTTP Operation
		http.perform();
		
		// close open file - avoids problems with renaming on GCS Buckets and other semi-POSIX systems
		if (file.isOpen()){
			file.close();
		}
		
		// Rename downloaded file
		rename(downloadFilename, originalFilename);

		// Update response and return response
		response.update(&http);
		return response;
	}

	// Cleanup this instance internal variables that may have been set
	void cleanup(bool flushCookies = false) {
		// Reset any values to defaults, freeing any set objects
		if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine cleanup() called on instance id: " ~ to!string(internalThreadId), ["debug"]);}
		
		// Is the instance is stopped?
		if (!http.isStopped) {
			// A stopped instance is not usable, these cannot be reset
			http.clearRequestHeaders();
			http.onSend = null;
			http.onReceive = null;
			http.onReceiveHeader = null;
			http.onReceiveStatusLine = null;
			http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) {
				return 0;
			};
			http.contentLength = 0;
			
			// We only do this if we are pushing the curl engine back to the curl pool
			if (flushCookies) {
				// Flush the cookie cache as well
				http.flushCookieJar();
				http.clearSessionCookies();
				http.clearAllCookies();
			}
		}
		
		// set the response to null
		response = null;

		// close file if open
		if (uploadFile.isOpen()){
			// close open file
			uploadFile.close();
		}
	}

	// Shut down the curl instance & close any open sockets
	void shutdownCurlHTTPInstance() {
		// Log that we are attempting to shutdown this curl instance
		if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine shutdownCurlHTTPInstance() called on instance id: " ~ to!string(internalThreadId), ["debug"]);}
		
		// Is this curl instance is stopped?
		if (!http.isStopped) {
			if ((debugLogging) && (debugHTTPSResponse)) {
				addLogEntry("HTTP instance still active: " ~ to!string(internalThreadId), ["debug"]);
				addLogEntry("HTTP instance isStopped state before http.shutdown(): " ~ to!string(http.isStopped), ["debug"]);
			}
			http.shutdown();
			if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("HTTP instance isStopped state post http.shutdown(): " ~ to!string(http.isStopped), ["debug"]);}
			object.destroy(http); // Destroy, however we cant set to null
			if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("HTTP instance shutdown and destroyed: " ~ to!string(internalThreadId), ["debug"]);}
			
		} else {
			// Already stopped .. destroy it
			object.destroy(http); // Destroy, however we cant set to null
			if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Stopped HTTP instance shutdown and destroyed: " ~ to!string(internalThreadId), ["debug"]);}
		}
		// Perform Garbage Collection
		GC.collect();
		// Return free memory to the OS
		GC.minimize();
	}
	
	// Disable SSL certificate peer verification for libcurl operations.
	//
	// This function disables the verification of the SSL peer's certificate
	// by setting CURLOPT_SSL_VERIFYPEER to 0. This means that libcurl will
	// accept any certificate presented by the server, regardless of whether
	// it is signed by a trusted certificate authority.
	//
	// -------------------------------------------------------------------------------------
	// WARNING: Disabling SSL peer verification introduces significant security risks:
	// -------------------------------------------------------------------------------------
	// - Man-in-the-Middle (MITM) attacks become trivially possible.
	// - Malicious servers can impersonate trusted endpoints.
	// - Confidential data (authentication tokens, file contents) can be intercepted.
	// - Violates industry security standards and regulatory compliance requirements.
	// - Should never be used in production environments or on untrusted networks.
	//
	// This option should only be enabled for internal testing, debugging self-signed
	// certificates, or explicitly controlled environments with known risks.
	//
	// See also:
	// https://curl.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html
	void setDisableSSLVerifyPeer() {
		// Emit a runtime warning if debug logging is enabled
		if (debugLogging) {
			addLogEntry("WARNING: SSL peer verification has been DISABLED!", ["debug"]);
			addLogEntry("         This allows invalid or self-signed certificates to be accepted.", ["debug"]);
			addLogEntry("         Use ONLY for testing. This severely weakens HTTPS security.", ["debug"]);
		}

		// Disable SSL certificate verification (DANGEROUS)
		http.handle.set(CurlOption.ssl_verifypeer, 0);
	}
	
	// Enable SSL Certificate Verification
	void setEnableSSLVerifyPeer() {
		// Enable SSL certificate verification
		addLogEntry("Enabling SSL peer verification");
		http.handle.set(CurlOption.ssl_verifypeer, 1);
	}
	
	// Set an applicable resumable offset point when downloading a file
	void setDownloadResumeOffset(long offset) {
		resumeFromOffset = offset;
	}
	
	// reset resumable offset point to negative value
	void resetDownloadResumeOffset() {
		resumeFromOffset = -1;
	}
}

// Methods to control obtaining and releasing a CurlEngine instance from the curlEnginePool

// Get a curl instance for the OneDrive API to use
CurlEngine getCurlInstance() {
	if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine getCurlInstance() called", ["debug"]);}
	
	synchronized (CurlEngine.classinfo) {
		// What is the current pool size
		if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool current size: " ~ to!string(curlEnginePool.length), ["debug"]);}
	
		if (curlEnginePool.empty) {
			if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool is empty - constructing a new CurlEngine instance", ["debug"]);}
			return new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance
		} else {
			CurlEngine curlEngine = curlEnginePool[$ - 1];
			curlEnginePool.popBack(); // assumes a LIFO (last-in, first-out) usage pattern
			
			// Is this engine stopped?
			if (curlEngine.http.isStopped) {
				// return a new curl engine as a stopped one cannot be used
				if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine was in a stopped state (not usable) - constructing a new CurlEngine instance", ["debug"]);}
				return new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance
			} else {
				// When was this engine last used?
				auto elapsedTime = Clock.currTime(UTC()) - curlEngine.releaseTimestamp;
				if ((debugLogging) && (debugHTTPSResponse)) {
					string engineIdleMessage = format("CurlEngine %s time since last use: %s", to!string(curlEngine.internalThreadId), to!string(elapsedTime));
					addLogEntry(engineIdleMessage, ["debug"]);
				}
				
				// If greater than 120 seconds (default), the treat this as a stale engine, preventing:
				// 	* Too old connection (xxx seconds idle), disconnect it
				// 	* Connection 0 seems to be dead!
				// 	* Closing connection 0
				
				if (elapsedTime > dur!"seconds"(curlEngine.maxIdleTime)) {
					// Too long idle engine, clean it up and create a new one
					if ((debugLogging) && (debugHTTPSResponse)) {
						string curlTooOldMessage = format("CurlEngine idle for > %d seconds .... destroying and returning a new curl engine instance", curlEngine.maxIdleTime);
						addLogEntry(curlTooOldMessage, ["debug"]);
					}
					
					curlEngine.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache
					curlEngine.shutdownCurlHTTPInstance();  // Assume proper cleanup of any resources used by HTTP
					if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("Returning NEW curlEngine instance", ["debug"]);}
					return new CurlEngine;  // Constructs a new CurlEngine with a fresh HTTP instance
				} else {
					// return an existing curl engine
					if ((debugLogging) && (debugHTTPSResponse)) {
						addLogEntry("CurlEngine was in a valid state - returning existing CurlEngine instance", ["debug"]);
						addLogEntry("Using CurlEngine instance ID: " ~ curlEngine.internalThreadId, ["debug"]);
					}
				
					// return the existing engine
					return curlEngine;
				}
			}
		}
	}
}

// Release all CurlEngine instances
void releaseAllCurlInstances() {
	if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine releaseAllCurlInstances() called", ["debug"]);}
	synchronized (CurlEngine.classinfo) {
		// What is the current pool size
		if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine curlEnginePool size to release: " ~ to!string(curlEnginePool.length), ["debug"]);}
		if (curlEnginePool.length > 0) {
			// Safely iterate and clean up each CurlEngine instance
			foreach (curlEngineInstance; curlEnginePool) {
				try {
					curlEngineInstance.cleanup(true); // Cleanup instance by resetting values and flushing cookie cache
					curlEngineInstance.shutdownCurlHTTPInstance();  // Assume proper cleanup of any resources used by HTTP
				} catch (Exception e) {
					// Log the error or handle it appropriately
					// e.g., writeln("Error during cleanup/shutdown: ", e.toString());
				}
				
				// It's safe to destroy the object here assuming no other references exist
				object.destroy(curlEngineInstance); // Destroy, then set to null
				curlEngineInstance = null;
				// Perform Garbage Collection on this destroyed curl engine
				GC.collect();
				// Log release
				if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine destroyed", ["debug"]);}
			}
		
			// Clear the array after all instances have been handled
			curlEnginePool.length = 0; // More explicit than curlEnginePool = [];
		}
	}
	// Perform Garbage Collection on the destroyed curl engines
	GC.collect();
	// Return free memory to the OS
	GC.minimize();
	// Log that all curl engines have been released
	if ((debugLogging) && (debugHTTPSResponse)) {addLogEntry("CurlEngine releaseAllCurlInstances() completed", ["debug"]);}
}

// Return how many curl engines there are
ulong curlEnginePoolLength() {
	return curlEnginePool.length;
}