1 /++
2     Bits and bobs that automate downloading SSL libraries and related necessities on Windows.
3 
4     TODO: Replace with Windows Secure Channel SSL.
5 
6     See_Also:
7         [kameloso.net]
8 
9     Copyright: [JR](https://github.com/zorael)
10     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
11 
12     Authors:
13         [JR](https://github.com/zorael)
14  +/
15 module kameloso.ssldownloads;
16 
17 version(Windows):
18 
19 private:
20 
21 import kameloso.kameloso : Kameloso;
22 import std.typecons : Flag, No, Yes;
23 
24 public:
25 
26 
27 // downloadWindowsSSL
28 /++
29     Downloads OpenSSL for Windows and/or a `cacert.pem` certificate bundle from
30     the cURL project, extracted from Mozilla Firefox.
31 
32     If `--force` was not supplied, the configuration file is updated with "`cacert.pem`"
33     entered as `caBundle`. If it is supplied, the value is still changed but to the
34     absolute path to the file, and the configuration file is not implicitly updated.
35     (`--save` will have to be separately passed.)
36 
37     Params:
38         instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso].
39         shouldDownloadCacert = Whether or not `cacert.pem` should be downloaded.
40         shouldDownloadOpenSSL = Whether or not OpenSSL for Windows should be downloaded.
41 
42     Returns:
43         `Yes.settingsTouched` if [kameloso.kameloso.Kameloso.settings|Kameloso.settings]
44         were touched and the configuration file should be updated; `No.settingsTouched` if not.
45  +/
46 auto downloadWindowsSSL(
47     ref Kameloso instance,
48     const Flag!"shouldDownloadCacert" shouldDownloadCacert,
49     const Flag!"shouldDownloadOpenSSL" shouldDownloadOpenSSL)
50 {
51     import kameloso.common : logger;
52     import std.path : buildNormalizedPath;
53 
54     static int downloadFile(const string url, const string what, const string saveAs)
55     {
56         import std.format : format;
57         import std.process : executeShell;
58 
59         enum pattern = "Downloading %s from <l>%s</>...";
60         logger.infof(pattern, what, url);
61 
62         enum executePattern = `powershell -c "Invoke-WebRequest '%s' -OutFile '%s'"`;
63         immutable result = executeShell(executePattern.format(url, saveAs));
64 
65         if (result.status != 0)
66         {
67             enum errorPattern = "Download process failed with status <l>%d</>!";
68             logger.errorf(errorPattern, result.status);
69 
70             version(PrintStacktraces)
71             {
72                 import std.stdio : stdout, writeln;
73                 import std.string : chomp;
74 
75                 immutable output = result.output.chomp;
76 
77                 if (output.length)
78                 {
79                     writeln(output);
80                     stdout.flush();
81                 }
82             }
83         }
84 
85         return result.status;
86     }
87 
88     Flag!"settingsTouched" retval;
89 
90     if (shouldDownloadCacert)
91     {
92         import kameloso.string : doublyBackslashed;
93         import std.path : dirName;
94 
95         enum cacertURL = "http://curl.se/ca/cacert.pem";
96         immutable configDir = instance.settings.configFile.dirName;
97         immutable cacertFile = buildNormalizedPath(configDir, "cacert.pem");
98         immutable result = downloadFile(cacertURL, "certificate bundle", cacertFile);
99         if (*instance.abort) return No.settingsTouched;
100 
101         if (result == 0)
102         {
103             if (!instance.settings.force)
104             {
105                 enum cacertPattern = "File saved as <l>%s</>; configuration updated.";
106                 logger.infof(cacertPattern, cacertFile.doublyBackslashed);
107                 instance.connSettings.caBundleFile = "cacert.pem";  // cacertFile
108                 retval = Yes.settingsTouched;
109             }
110             else
111             {
112                 enum cacertPattern = "File saved as <l>%s</>.";
113                 logger.infof(cacertPattern, cacertFile.doublyBackslashed);
114                 instance.connSettings.caBundleFile = cacertFile;  // absolute path
115                 //retval = Yes.settingsTouched;  // let user supply --save
116             }
117         }
118     }
119 
120     if (shouldDownloadOpenSSL)
121     {
122         import std.file : mkdirRecurse, tempDir;
123         import std.json : JSONException;
124         import std.process : ProcessException;
125 
126         immutable temporaryDir = buildNormalizedPath(tempDir, "kameloso");
127         mkdirRecurse(temporaryDir);
128 
129         enum jsonURL = "https://raw.githubusercontent.com/slproweb/opensslhashes/master/win32_openssl_hashes.json";
130         immutable jsonFile = buildNormalizedPath(temporaryDir, "win32_openssl_hashes.json");
131         immutable result = downloadFile(jsonURL, "manifest", jsonFile);
132         if (*instance.abort) return No.settingsTouched;
133         if (result != 0) return retval;
134 
135         try
136         {
137             import std.file : readText;
138             import std.json : parseJSON;
139 
140             const hashesJSON = parseJSON(readText(jsonFile));
141 
142             foreach (immutable filename, fileEntryJSON; hashesJSON["files"].object)
143             {
144                 import lu.string : beginsWith;
145                 import std.algorithm.searching : endsWith;
146 
147                 version(Win64)
148                 {
149                     enum head = "Win64OpenSSL_Light-1_";
150                 }
151                 else /*version(Win32)*/
152                 {
153                     enum head = "Win32OpenSSL_Light-1_";
154                 }
155 
156                 if (filename.beginsWith(head) && filename.endsWith(".exe"))
157                 {
158                     import std.process : execute;
159 
160                     immutable exeFile = buildNormalizedPath(temporaryDir, filename);
161                     immutable downloadResult = downloadFile(fileEntryJSON["url"].str, "OpenSSL installer", exeFile);
162                     if (*instance.abort) return No.settingsTouched;
163                     if (downloadResult != 0) break;
164 
165                     logger.info("Launching installer.");
166                     cast(void)execute([ exeFile ]);
167 
168                     return retval;
169                 }
170             }
171 
172             logger.error("Could not find OpenSSL .exe to download");
173             // Drop down and return
174         }
175         catch (JSONException e)
176         {
177             enum pattern = "Error parsing file containing OpenSSL download links: <l>%s";
178             logger.errorf(pattern, e.msg);
179         }
180         catch (ProcessException e)
181         {
182             enum pattern = "Error starting installer: <l>%s";
183             logger.errorf(pattern, e.msg);
184         }
185     }
186 
187     return retval;
188 }