Skip to content

Commit 9d42114

Browse files
committed
Add Win32::HttpGetFile
Discussions around mitigating the recent CVEs against PAUSE/CPAN (CVE-2020-16154, CVE-2020-16155, and CVE-2020-16156) included the desire to make Perl core able to do secure downloads out of the box, and further discussion that mechanisms based on OpenSSL, curl, or wget may not be available on Windows. See, for example: https://www.nntp.perl.org/group/perl.perl5.porters/2021/12/msg262180.html This commit adds a native download function to the Win32 module based on the WinHttp library, a function that could in turn be used by CPAN.pm, etc. N.B. The autoproxy detection code has not been tested since I don't have a set-up where I can do that.
1 parent ff8e050 commit 9d42114

File tree

4 files changed

+283
-2
lines changed

4 files changed

+283
-2
lines changed

Makefile.PL

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ my %param = (
2222
$param{NO_META} = 1 if eval "$ExtUtils::MakeMaker::VERSION" >= 6.10_03;
2323

2424
if ($^O eq 'cygwin') {
25-
$param{LIBS} = ['-L/lib/w32api -lole32 -lversion -luserenv -lnetapi32']
25+
$param{LIBS} = ['-L/lib/w32api -lole32 -lversion -luserenv -lnetapi32 -lwinhttp']
2626
}
2727
else {
28-
$param{LIBS} = ['-luserenv']
28+
$param{LIBS} = ['-luserenv -lwinhttp']
2929
}
3030

3131
my $test_requires = $ExtUtils::MakeMaker::VERSION >= 6.64

Win32.pm

+6
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,12 @@ instead.
14721472
Loads the DLL LIBRARYNAME and calls the function
14731473
DllUnregisterServer.
14741474
1475+
=item Win32::HttpGetFile(URL, FILENAME)
1476+
1477+
Uses the WinHttp library to download the file specified by the URL
1478+
parameter to the local file specified by FILENAME. Only http and https
1479+
protocols are supported. Authentication is not supported.
1480+
14751481
=back
14761482
14771483
=head1 CAVEATS

Win32.xs

+244
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <wchar.h>
88
#include <userenv.h>
99
#include <lm.h>
10+
#include <winhttp.h>
1011

1112
#define PERL_NO_GET_CONTEXT
1213
#include "EXTERN.h"
@@ -1682,6 +1683,248 @@ XS(w32_IsDeveloperModeEnabled)
16821683
XSRETURN_NO;
16831684
}
16841685

1686+
1687+
XS(w32_HttpGetFile)
1688+
{
1689+
dXSARGS;
1690+
WCHAR *url = NULL, *file = NULL, *hostName = NULL, *urlPath = NULL;
1691+
DWORD dwSize = 0;
1692+
DWORD dwDownloaded = 0;
1693+
DWORD dwBytesWritten = 0;
1694+
LPSTR pszOutBuffer;
1695+
BOOL bResults = FALSE;
1696+
HINTERNET hSession = NULL,
1697+
hConnect = NULL,
1698+
hRequest = NULL;
1699+
HANDLE hOut = NULL;
1700+
BOOL bParsed = FALSE,
1701+
bAborted = FALSE,
1702+
bFileError = FALSE;
1703+
DWORD error = 0;
1704+
URL_COMPONENTS urlComp;
1705+
LPCWSTR acceptTypes[] = { L"*/*", NULL };
1706+
WINHTTP_AUTOPROXY_OPTIONS AutoProxyOptions;
1707+
WINHTTP_PROXY_INFO ProxyInfo;
1708+
DWORD cbProxyInfoSize = sizeof(ProxyInfo);
1709+
1710+
if (items != 2)
1711+
croak("usage: Win32::HttpGetFile($url, $file)");
1712+
1713+
url = sv_to_wstr(aTHX_ ST(0));
1714+
file = sv_to_wstr(aTHX_ ST(1));
1715+
1716+
/* Initialize the URL_COMPONENTS structure, setting the required
1717+
* component lengths to non-zero so that they get populated.
1718+
*/
1719+
ZeroMemory(&urlComp, sizeof(urlComp));
1720+
urlComp.dwStructSize = sizeof(urlComp);
1721+
urlComp.dwSchemeLength = (DWORD)-1;
1722+
urlComp.dwHostNameLength = (DWORD)-1;
1723+
urlComp.dwUrlPathLength = (DWORD)-1;
1724+
urlComp.dwExtraInfoLength = (DWORD)-1;
1725+
1726+
/* Parse the URL. */
1727+
bParsed = WinHttpCrackUrl(url, (DWORD)wcslen(url), 0, &urlComp);
1728+
1729+
/* Only support http and htts, not ftp, gopher, etc. */
1730+
if (bParsed
1731+
&& !(urlComp.nScheme == INTERNET_SCHEME_HTTPS
1732+
|| urlComp.nScheme == INTERNET_SCHEME_HTTP)) {
1733+
SetLastError(12006); /* not a recognized protocol */
1734+
bParsed = FALSE;
1735+
}
1736+
1737+
if (bParsed) {
1738+
New(0, hostName, urlComp.dwHostNameLength + 1, WCHAR);
1739+
wcsncpy(hostName, urlComp.lpszHostName, urlComp.dwHostNameLength);
1740+
hostName[urlComp.dwHostNameLength] = 0;
1741+
1742+
New(0, urlPath, urlComp.dwUrlPathLength + urlComp.dwExtraInfoLength + 1, WCHAR);
1743+
wcsncpy(urlPath, urlComp.lpszUrlPath, urlComp.dwUrlPathLength + urlComp.dwExtraInfoLength);
1744+
urlPath[urlComp.dwUrlPathLength + urlComp.dwExtraInfoLength] = 0;
1745+
1746+
/* Use WinHttpOpen to obtain a session handle. */
1747+
hSession = WinHttpOpen(L"Perl",
1748+
WINHTTP_ACCESS_TYPE_NO_PROXY,
1749+
WINHTTP_NO_PROXY_NAME,
1750+
WINHTTP_NO_PROXY_BYPASS,
1751+
0);
1752+
}
1753+
1754+
/* Specify an HTTP server. */
1755+
if (hSession)
1756+
hConnect = WinHttpConnect(hSession,
1757+
hostName,
1758+
urlComp.nPort,
1759+
0);
1760+
1761+
/* Create an HTTP request handle. */
1762+
if (hConnect)
1763+
hRequest = WinHttpOpenRequest(hConnect,
1764+
L"GET",
1765+
urlPath,
1766+
NULL,
1767+
WINHTTP_NO_REFERER,
1768+
acceptTypes,
1769+
urlComp.nScheme == INTERNET_SCHEME_HTTPS
1770+
? WINHTTP_FLAG_SECURE
1771+
: 0);
1772+
1773+
/* Call WinHttpGetProxyForUrl with our target URL. If auto-proxy succeeds,
1774+
* then set the proxy info on the request handle. If auto-proxy fails,
1775+
* ignore the error and attempt to send the HTTP request directly to the
1776+
* target server (using the default WINHTTP_ACCESS_TYPE_NO_PROXY
1777+
* configuration, which the request handle will inherit from the session).
1778+
*/
1779+
if (hRequest) {
1780+
ZeroMemory(&AutoProxyOptions, sizeof(AutoProxyOptions));
1781+
ZeroMemory(&ProxyInfo, sizeof(ProxyInfo));
1782+
AutoProxyOptions.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
1783+
AutoProxyOptions.dwAutoDetectFlags =
1784+
WINHTTP_AUTO_DETECT_TYPE_DHCP |
1785+
WINHTTP_AUTO_DETECT_TYPE_DNS_A;
1786+
AutoProxyOptions.fAutoLogonIfChallenged = TRUE;
1787+
1788+
if(WinHttpGetProxyForUrl(hSession,
1789+
url,
1790+
&AutoProxyOptions,
1791+
&ProxyInfo)) {
1792+
if(!WinHttpSetOption(hRequest,
1793+
WINHTTP_OPTION_PROXY,
1794+
&ProxyInfo,
1795+
cbProxyInfoSize)) {
1796+
bAborted = TRUE;
1797+
Perl_warn(aTHX_ "Win32::HttpGetFile: setting proxy options failed");
1798+
}
1799+
Safefree(ProxyInfo.lpszProxy);
1800+
Safefree(ProxyInfo.lpszProxyBypass);
1801+
}
1802+
}
1803+
1804+
/* Send a request. */
1805+
if (hRequest && !bAborted)
1806+
bResults = WinHttpSendRequest(hRequest,
1807+
WINHTTP_NO_ADDITIONAL_HEADERS,
1808+
0,
1809+
WINHTTP_NO_REQUEST_DATA,
1810+
0,
1811+
0,
1812+
0);
1813+
1814+
/* End the request. */
1815+
if (bResults)
1816+
bResults = WinHttpReceiveResponse(hRequest, NULL);
1817+
1818+
/* Create output file for download. */
1819+
if (bResults) {
1820+
hOut = CreateFileW(file,
1821+
GENERIC_WRITE,
1822+
FILE_SHARE_READ | FILE_SHARE_WRITE,
1823+
NULL,
1824+
CREATE_ALWAYS,
1825+
FILE_ATTRIBUTE_NORMAL,
1826+
NULL);
1827+
1828+
if (!hOut || hOut == INVALID_HANDLE_VALUE)
1829+
bFileError = TRUE;
1830+
}
1831+
1832+
/* Keep checking for data until there is nothing left. */
1833+
if (!bFileError && bResults) {
1834+
do {
1835+
/* Check for available data. */
1836+
dwSize = 0;
1837+
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
1838+
bAborted = TRUE;
1839+
break;
1840+
}
1841+
1842+
/* No more available data. */
1843+
if (!dwSize)
1844+
break;
1845+
1846+
/* Allocate space for the buffer. */
1847+
New(0, pszOutBuffer, dwSize + 1, char);
1848+
if (!pszOutBuffer) {
1849+
bAborted = TRUE;
1850+
break;
1851+
}
1852+
1853+
/* Read the Data. */
1854+
ZeroMemory(pszOutBuffer, dwSize+1);
1855+
1856+
if (!WinHttpReadData(hRequest,
1857+
(LPVOID)pszOutBuffer,
1858+
dwSize,
1859+
&dwDownloaded)) {
1860+
bAborted = TRUE;
1861+
Safefree(pszOutBuffer);
1862+
break;
1863+
}
1864+
1865+
/* Write what we just read to the output file */
1866+
if (!WriteFile(hOut,
1867+
pszOutBuffer,
1868+
dwDownloaded,
1869+
&dwBytesWritten,
1870+
NULL)) {
1871+
bAborted = TRUE;
1872+
bFileError = TRUE;
1873+
Safefree(pszOutBuffer);
1874+
break;
1875+
}
1876+
1877+
Safefree(pszOutBuffer);
1878+
1879+
/* This condition would only be reached if WinHttpQueryDataAvailable
1880+
* said there are more data to read but WinHttpReadData didn't get any.
1881+
*/
1882+
if (!dwDownloaded)
1883+
break;
1884+
}
1885+
while (dwSize > 0);
1886+
}
1887+
else {
1888+
bAborted = TRUE;
1889+
}
1890+
1891+
/* Clean-up may lose this. */
1892+
if (bAborted)
1893+
error = GetLastError();
1894+
1895+
/* Close any open handles. */
1896+
if (hOut) CloseHandle(hOut);
1897+
if (hRequest) WinHttpCloseHandle(hRequest);
1898+
if (hConnect) WinHttpCloseHandle(hConnect);
1899+
if (hSession) WinHttpCloseHandle(hSession);
1900+
1901+
Safefree(url);
1902+
Safefree(file);
1903+
Safefree(hostName);
1904+
Safefree(urlPath);
1905+
1906+
if (bAborted) {
1907+
char msgbuf[ONE_K_BUFSIZE];
1908+
DWORD msgFlags = bFileError
1909+
? FORMAT_MESSAGE_FROM_SYSTEM
1910+
: FORMAT_MESSAGE_FROM_HMODULE;
1911+
1912+
if (FormatMessageA(msgFlags,
1913+
GetModuleHandleA("winhttp.dll"),
1914+
error,
1915+
0,
1916+
msgbuf,
1917+
sizeof(msgbuf) - 1,
1918+
NULL)) {
1919+
Perl_warn(aTHX_ "Error %lu in Win32::HttpGetFile: %s", error, msgbuf);
1920+
}
1921+
SetLastError(error);
1922+
XSRETURN_NO;
1923+
}
1924+
1925+
XSRETURN_YES;
1926+
}
1927+
16851928
MODULE = Win32 PACKAGE = Win32
16861929

16871930
PROTOTYPES: DISABLE
@@ -1756,5 +1999,6 @@ BOOT:
17561999
#ifdef __CYGWIN__
17572000
newXS("Win32::SetChildShowWindow", w32_SetChildShowWindow, file);
17582001
#endif
2002+
newXS("Win32::HttpGetFile", w32_HttpGetFile, file);
17592003
XSRETURN_YES;
17602004
}

t/HttpGetFile.t

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use strict;
2+
use warnings;
3+
use Test;
4+
use Win32;
5+
use Digest::SHA;
6+
7+
my $tmpfile = "http-download-test-$$.tgz";
8+
END { 1 while unlink $tmpfile; }
9+
10+
# We may not always have an internet connection, so don't
11+
# attempt remote connections unless the user has done
12+
# set PERL_WIN32_INTERNET_OK=1
13+
plan tests => $ENV{PERL_WIN32_INTERNET_OK} ? 6 : 4;
14+
15+
ok(Win32::HttpGetFile('nonesuch://example.com', 'NUL:'), "", "'nonesuch://' is not a real protocol");
16+
ok(Win32::GetLastError(), '12006', "correct error code for unrecognized protocol");
17+
ok(Win32::HttpGetFile('http://!#@!&@$', 'NUL:'), "", "invalid URL");
18+
ok(Win32::GetLastError(), '12005', "correct error code for invalid URL");
19+
20+
if ($ENV{PERL_WIN32_INTERNET_OK}) {
21+
# The digest for version 0.57 should obviously stay the same even after new versions are released
22+
ok(Win32::HttpGetFile('https://cpan.metacpan.org/authors/id/J/JD/JDB/Win32-0.57.tar.gz', $tmpfile),
23+
'1',
24+
"successfully downloaded a tarball");
25+
26+
my $sha = Digest::SHA->new('sha1');
27+
$sha->addfile($tmpfile, 'b');
28+
ok($sha->hexdigest,
29+
'44a6d7d1607d7267b0dbcacbb745cec204f1c1a4',
30+
"downloaded tarball has correct digest");
31+
}

0 commit comments

Comments
 (0)