Skip to content

Commit 88807a7

Browse files
committed
Add list context return to Win32::HttpGetFile
Don't croak or warn, but make the status message available along with the return status when called in list context. In scalar context, we'll still just get thumbs up or thumbs down, but can get some idea of what went wrong on failure from Win32::GetLastError(), although we won't know whether that's a system error or WinHttp error. Win32::HttpGetFile optionally ignores certificate errors Make Win32::HttpGetFile fail on HTTP failure There's not much point in successfully downloading an error page from the server and saving it to a file, and in fact it's a bit unfriendly because the user would be unlikely to know there is a problem until inspecting file contents some time later.
1 parent 2970ec9 commit 88807a7

File tree

3 files changed

+147
-25
lines changed

3 files changed

+147
-25
lines changed

Win32.pm

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,19 +1305,47 @@ of hex digits with surrounding braces. For example:
13051305
13061306
{09531CF1-D0C7-4860-840C-1C8C8735E2AD}
13071307
1308-
=item Win32::HttpGetFile(URL, FILENAME)
1308+
=item Win32::HttpGetFile(URL, FILENAME [, IGNORE_CERT_ERRORS])
13091309
13101310
Uses the WinHttp library to download the file specified by the URL
1311-
parameter to the local file specified by FILENAME. Only http and https
1312-
protocols are supported. Authentication is not supported. The function
1313-
is not available when building with gcc prior to 4.8.0 because the
1314-
winhttp library is not available.
1311+
parameter to the local file specified by FILENAME. The optional third
1312+
parameter, if true, indicates that certficate errors are to be ignored
1313+
for https connections; please use with caution in a safe environment,
1314+
such as when testing locally using a self-signed certificate.
1315+
1316+
Only http and https protocols are supported. Authentication is not
1317+
supported. The function is not available when building with gcc prior to
1318+
4.8.0 because the WinHttp library is not available.
1319+
1320+
In scalar context returns a boolean success or failure, and in list
1321+
context also returns, in addition to the boolean status, a second
1322+
value containing message text related to the status.
1323+
1324+
If the call fails, C<Win32::GetLastError()> will return a numeric
1325+
error code, which may be either a system error or a WinHttp error.
1326+
1327+
Scalar context example:
1328+
1329+
print Win32::GetLastError()
1330+
unless Win32::HttpGetFile('http://example.com/somefile.tar.gz',
1331+
'.\file.tgz');
1332+
1333+
List context example:
1334+
1335+
my ($ok, $msg) = Win32::HttpGetFile('http://example.com/somefile.tar.gz',
1336+
'.\file.tgz');
1337+
if ($ok) {
1338+
print "Success!: $msg\n";
1339+
}
1340+
else {
1341+
print "Failure!: $msg\n";
1342+
}
13151343
13161344
=item Win32::InitiateSystemShutdown
13171345
13181346
(MACHINE, MESSAGE, TIMEOUT, FORCECLOSE, REBOOT)
13191347
1320-
Shutsdown the specified MACHINE, notifying users with the
1348+
Shuts down the specified MACHINE, notifying users with the
13211349
supplied MESSAGE, within the specified TIMEOUT interval. Forces
13221350
closing of all documents without prompting the user if FORCECLOSE is
13231351
true, and reboots the machine if REBOOT is true. This function works

Win32.xs

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,24 +1691,31 @@ XS(w32_HttpGetFile)
16911691
{
16921692
dXSARGS;
16931693
WCHAR *url = NULL, *file = NULL, *hostName = NULL, *urlPath = NULL;
1694+
bool bIgnoreCertErrors = FALSE;
1695+
WCHAR msgbuf[ONE_K_BUFSIZE];
16941696
BOOL bResults = FALSE;
16951697
HINTERNET hSession = NULL,
16961698
hConnect = NULL,
16971699
hRequest = NULL;
16981700
HANDLE hOut = NULL;
16991701
BOOL bParsed = FALSE,
17001702
bAborted = FALSE,
1701-
bFileError = FALSE;
1703+
bFileError = FALSE,
1704+
bHttpError = FALSE;
17021705
DWORD error = 0;
17031706
URL_COMPONENTS urlComp;
17041707
LPCWSTR acceptTypes[] = { L"*/*", NULL };
1708+
DWORD dwHttpStatusCode = 0, dwQuerySize = 0;
17051709

1706-
if (items != 2)
1707-
croak("usage: Win32::HttpGetFile($url, $file)");
1710+
if (items < 2 || items > 3)
1711+
croak("usage: Win32::HttpGetFile($url, $file[, $ignore_cert_errors])");
17081712

17091713
url = sv_to_wstr(aTHX_ ST(0));
17101714
file = sv_to_wstr(aTHX_ ST(1));
17111715

1716+
if (items == 3)
1717+
bIgnoreCertErrors = (BOOL)SvIV(ST(2));
1718+
17121719
/* Initialize the URL_COMPONENTS structure, setting the required
17131720
* component lengths to non-zero so that they get populated.
17141721
*/
@@ -1766,13 +1773,29 @@ XS(w32_HttpGetFile)
17661773
? WINHTTP_FLAG_SECURE
17671774
: 0);
17681775

1776+
/* If specified, disable certificate-related errors for https connections. */
1777+
if (hRequest
1778+
&& bIgnoreCertErrors
1779+
&& urlComp.nScheme == INTERNET_SCHEME_HTTPS) {
1780+
DWORD secFlags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID
1781+
| SECURITY_FLAG_IGNORE_CERT_DATE_INVALID
1782+
| SECURITY_FLAG_IGNORE_UNKNOWN_CA
1783+
| SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;
1784+
if(!WinHttpSetOption(hRequest,
1785+
WINHTTP_OPTION_SECURITY_FLAGS,
1786+
&secFlags,
1787+
sizeof(secFlags))) {
1788+
bAborted = TRUE;
1789+
}
1790+
}
1791+
17691792
/* Call WinHttpGetProxyForUrl with our target URL. If auto-proxy succeeds,
17701793
* then set the proxy info on the request handle. If auto-proxy fails,
17711794
* ignore the error and attempt to send the HTTP request directly to the
17721795
* target server (using the default WINHTTP_ACCESS_TYPE_NO_PROXY
17731796
* configuration, which the request handle will inherit from the session).
17741797
*/
1775-
if (hRequest) {
1798+
if (hRequest && !bAborted) {
17761799
WINHTTP_AUTOPROXY_OPTIONS AutoProxyOptions;
17771800
WINHTTP_PROXY_INFO ProxyInfo;
17781801
DWORD cbProxyInfoSize = sizeof(ProxyInfo);
@@ -1815,6 +1838,39 @@ XS(w32_HttpGetFile)
18151838
if (bResults)
18161839
bResults = WinHttpReceiveResponse(hRequest, NULL);
18171840

1841+
/* Retrieve HTTP status code. */
1842+
if (bResults) {
1843+
dwQuerySize = sizeof(dwHttpStatusCode);
1844+
bResults = WinHttpQueryHeaders(hRequest,
1845+
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
1846+
WINHTTP_HEADER_NAME_BY_INDEX,
1847+
&dwHttpStatusCode,
1848+
&dwQuerySize,
1849+
WINHTTP_NO_HEADER_INDEX);
1850+
}
1851+
1852+
/* Retrieve HTTP status text. Note this may be a success message. */
1853+
if (bResults) {
1854+
dwQuerySize = ONE_K_BUFSIZE * 2 - 2;
1855+
ZeroMemory(&msgbuf, ONE_K_BUFSIZE * 2);
1856+
bResults = WinHttpQueryHeaders(hRequest,
1857+
WINHTTP_QUERY_STATUS_TEXT,
1858+
WINHTTP_HEADER_NAME_BY_INDEX,
1859+
msgbuf,
1860+
&dwQuerySize,
1861+
WINHTTP_NO_HEADER_INDEX);
1862+
}
1863+
1864+
/* There is no point in successfully downloading an error page from
1865+
* the server, so consider HTTP errors to be failures.
1866+
*/
1867+
if (bResults) {
1868+
if (dwHttpStatusCode < 200 || dwHttpStatusCode > 299) {
1869+
bResults = FALSE;
1870+
bHttpError = TRUE;
1871+
}
1872+
}
1873+
18181874
/* Create output file for download. */
18191875
if (bResults) {
18201876
hOut = CreateFileW(file,
@@ -1883,26 +1939,42 @@ XS(w32_HttpGetFile)
18831939
Safefree(hostName);
18841940
Safefree(urlPath);
18851941

1886-
if (bAborted) {
1887-
char msgbuf[ONE_K_BUFSIZE];
1942+
/* Retrieve system and WinHttp error messages, but not if we already
1943+
* got a failed HTTP status text above.
1944+
*/
1945+
if (bAborted && !bHttpError) {
18881946
DWORD msgFlags = bFileError
18891947
? FORMAT_MESSAGE_FROM_SYSTEM
18901948
: FORMAT_MESSAGE_FROM_HMODULE;
1891-
1892-
if (FormatMessageA(msgFlags,
1893-
GetModuleHandleA("winhttp.dll"),
1894-
error,
1895-
0,
1896-
msgbuf,
1897-
sizeof(msgbuf) - 1,
1898-
NULL)) {
1899-
Perl_warn(aTHX_ "Error %lu in Win32::HttpGetFile: %s", error, msgbuf);
1949+
msgFlags |= FORMAT_MESSAGE_IGNORE_INSERTS;
1950+
1951+
ZeroMemory(&msgbuf, ONE_K_BUFSIZE * 2);
1952+
if (!FormatMessageW(msgFlags,
1953+
GetModuleHandleW(L"winhttp.dll"),
1954+
error,
1955+
0,
1956+
msgbuf,
1957+
ONE_K_BUFSIZE - 1, /* TCHARs, not bytes */
1958+
NULL)) {
1959+
wcsncpy(msgbuf, L"unable to format error message", ONE_K_BUFSIZE - 1);
19001960
}
19011961
SetLastError(error);
1902-
XSRETURN_NO;
19031962
}
19041963

1905-
XSRETURN_YES;
1964+
if (GIMME_V == G_SCALAR) {
1965+
EXTEND(SP, 1);
1966+
ST(0) = !bAborted ? &PL_sv_yes : &PL_sv_no;
1967+
XSRETURN(1);
1968+
}
1969+
else if (GIMME_V == G_ARRAY) {
1970+
EXTEND(SP, 2);
1971+
ST(0) = !bAborted ? &PL_sv_yes : &PL_sv_no;
1972+
ST(1) = wstr_to_sv(aTHX_ msgbuf);
1973+
XSRETURN(2);
1974+
}
1975+
else {
1976+
XSRETURN_EMPTY;
1977+
}
19061978
}
19071979

19081980
#endif

t/HttpGetFile.t

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use warnings;
33
use Test;
44
use Win32;
55
use Digest::SHA;
6+
use POSIX qw(locale_h);
7+
setlocale(LC_ALL, "C"); # to make error messages predictable
68

79
my $tmpfile = "http-download-test-$$.tgz";
810
END { 1 while unlink $tmpfile; }
@@ -15,7 +17,7 @@ unless (defined &Win32::HttpGetFile) {
1517
# We may not always have an internet connection, so don't
1618
# attempt remote connections unless the user has done
1719
# set PERL_WIN32_INTERNET_OK=1
18-
plan tests => $ENV{PERL_WIN32_INTERNET_OK} ? 6 : 4;
20+
plan tests => $ENV{PERL_WIN32_INTERNET_OK} ? 12 : 7;
1921

2022
# On Cygwin the test_harness will invoke additional Win32 APIs that
2123
# will reset the Win32::GetLastError() value, so capture it immediately.
@@ -26,11 +28,22 @@ sub HttpGetFile {
2628
return $ok;
2729
}
2830

31+
sub HttpGetFileList {
32+
my ($ok, $message) = Win32::HttpGetFile(@_);
33+
$LastError = Win32::GetLastError();
34+
return ($ok, $message);
35+
}
36+
2937
ok(HttpGetFile('nonesuch://example.com', 'NUL:'), "", "'nonesuch://' is not a real protocol");
3038
ok($LastError, '12006', "correct error code for unrecognized protocol");
3139
ok(HttpGetFile('http://!#@!&@$', 'NUL:'), "", "invalid URL");
3240
ok($LastError, '12005', "correct error code for invalid URL");
3341

42+
my ($ok, $message) = HttpGetFileList('nonesuch://example.com', 'NUL:');
43+
ok($ok, "", "'nonesuch://' is not a real protocol");
44+
ok($message, "The URL does not use a recognized protocol\r\n", "correct bad protocol message");
45+
ok($LastError, '12006', "correct error code for unrecognized protocol with list context return");
46+
3447
if ($ENV{PERL_WIN32_INTERNET_OK}) {
3548
# The digest for version 0.57 should obviously stay the same even after new versions are released
3649
ok(Win32::HttpGetFile('https://cpan.metacpan.org/authors/id/J/JD/JDB/Win32-0.57.tar.gz', $tmpfile),
@@ -42,4 +55,13 @@ if ($ENV{PERL_WIN32_INTERNET_OK}) {
4255
ok($sha->hexdigest,
4356
'44a6d7d1607d7267b0dbcacbb745cec204f1c1a4',
4457
"downloaded tarball has correct digest");
45-
}
58+
59+
my ($ok, $message) = HttpGetFileList('https://cpan.metacpan.org/authors/id/Z/ZZ/ZILCH/nonesuch.tar.gz', 'NUL:');
60+
ok($ok, '', 'Download of nonexistent file from real site should fail with 404');
61+
ok($message, 'Not Found', 'Should get text of 404 message');
62+
63+
# Since all GitHub downloads use redirects, we can test that they work.
64+
ok(Win32::HttpGetFile('https://github.com/perl-libwin32/win32/archive/refs/tags/v0.57.zip', $tmpfile),
65+
'1',
66+
"successfully downloaded a zipball via redirect");
67+
}

0 commit comments

Comments
 (0)