Bringing WiFi to the Cidco Mailstation
1/*
2 * WiFiStation
3 * Copyright (c) 2021 joshua stein <jcs@jcs.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <WiFiClient.h>
19#include <WiFiClientSecure.h>
20#include "wifistation.h"
21
22static const char OTA_VERSION_URL[] PROGMEM =
23 "https://raw.githubusercontent.com/jcs/WiFiStation/main/release/version.txt";
24
25static WiFiClient client;
26static WiFiClientSecure client_tls;
27static bool tls = false;
28
29bool
30update_http_read_until_body(const char *url, long expected_length)
31{
32 char *host, *path, *colon;
33 int status = 0, port, httpver, chars, lines, tlength, clength = -1;
34
35 if (WiFi.status() != WL_CONNECTED) {
36 output("ERROR WiFi is not connected\r\n");
37 return false;
38 }
39
40 if (url == NULL) {
41 outputf("ERROR failed parsing NULL URL\r\n");
42 return false;
43 }
44
45 host = (char *)malloc(strlen(url) + 1);
46 if (host == NULL) {
47 output("ERROR malloc failed\r\n");
48 return false;
49 }
50
51 path = (char *)malloc(strlen(url) + 1);
52 if (path == NULL) {
53 output("ERROR malloc failed\r\n");
54 free(host);
55 return false;
56 }
57
58 if (sscanf(url, "http://%[^/]%s%n", host, path, &chars) == 2 &&
59 chars != 0) {
60 tls = false;
61 } else if (sscanf(url, "https://%[^/]%s%n", host, path, &chars) == 2
62 && chars != 0) {
63 tls = true;
64 /*
65 * This would be nice to not have to do, but keeping up with
66 * GitHub's TLS cert fingerprints will be tedious, and we have
67 * no cert chain.
68 */
69 client_tls.setInsecure();
70 } else {
71 outputf("ERROR failed parsing URL \"%s\"\r\n", url);
72 free(path);
73 free(host);
74 return false;
75 }
76
77 if ((colon = strchr(host, ':'))) {
78 colon[0] = '\0';
79 port = atoi(colon + 1);
80 } else {
81 port = (tls ? 443 : 80);
82 }
83
84#ifdef UPDATE_TRACE
85 syslog.logf(LOG_DEBUG, "%s: host \"%s\" path \"%s\" port %d tls %d",
86 __func__, host, path, port, tls ? 1 : 0);
87#endif
88
89 if (!(tls ? client_tls : client).connect(host, port)) {
90 outputf("ERROR OTA failed connecting to http%s://%s:%d\r\n",
91 tls ? "s" : "", host, port);
92 free(path);
93 free(host);
94 return false;
95 }
96
97 (tls ? client_tls : client).printf("GET %s HTTP/1.0\r\n", path);
98 (tls ? client_tls : client).printf("Host: %s\r\n", host);
99 (tls ? client_tls : client).printf("User-Agent: WiFiStation %s\r\n",
100 WIFISTATION_VERSION);
101 (tls ? client_tls : client).printf("Connection: close\r\n");
102 (tls ? client_tls : client).printf("\r\n");
103
104 free(path);
105 free(host);
106
107 /* read headers */
108 lines = 0;
109 while ((tls ? client_tls : client).connected() ||
110 (tls ? client_tls : client).available()) {
111 String line = (tls ? client_tls : client).readStringUntil('\n');
112
113#ifdef UPDATE_TRACE
114 syslog.logf(LOG_DEBUG, "%s: read header \"%s\"", __func__,
115 line.c_str());
116#endif
117
118 if (lines == 0)
119 sscanf(line.c_str(), "HTTP/1.%d %d%n", &httpver,
120 &status, &chars);
121 else if (sscanf(line.c_str(), "Content-Length: %d%n",
122 &tlength, &chars) == 1 && chars > 0)
123 clength = tlength;
124 else if (line == "\r")
125 break;
126
127 lines++;
128 }
129
130#ifdef UPDATE_TRACE
131 syslog.logf(LOG_DEBUG, "%s: read status %d, content-length %d vs "
132 "expected %ld", __func__, status, clength, expected_length);
133#endif
134
135 if (status != 200) {
136 outputf("ERROR OTA fetch of %s failed with HTTP status %d\r\n",
137 url, status);
138 goto drain;
139 }
140
141 if (expected_length != 0 && clength != expected_length) {
142 outputf("ERROR OTA fetch of %s expected to be size %d, "
143 "fetched %d\r\n", url, expected_length, clength);
144 goto drain;
145 }
146
147 while ((tls ? client_tls : client).connected() &&
148 !(tls ? client_tls : client).available())
149 ESP.wdtFeed();
150
151 return true;
152
153drain:
154#ifdef UPDATE_TRACE
155 syslog.logf(LOG_DEBUG, "%s: draining remaining body", __func__);
156#endif
157 while ((tls ? client_tls : client).available())
158 (tls ? client_tls : client).read();
159 (tls ? client_tls : client).stop();
160 return false;
161}
162
163void
164update_process(char *url, bool do_update, bool force)
165{
166 String rom_url, md5, version;
167 int bytesize = 0, lines = 0, len;
168 char *furl = NULL;
169
170 output("\n");
171
172 if (url == NULL) {
173 furl = url = (char *)malloc(len = (strlen_P(OTA_VERSION_URL) +
174 1));
175 if (url == NULL) {
176 output("ERROR malloc failed\r\n");
177 return;
178 }
179 memcpy_P(url, OTA_VERSION_URL, len);
180 }
181
182#ifdef UPDATE_TRACE
183 syslog.logf(LOG_DEBUG, "fetching update manifest from \"%s\"", url);
184#endif
185
186 if (!update_http_read_until_body(url, 0)) {
187#ifdef UPDATE_TRACE
188 syslog.logf(LOG_DEBUG, "read until body(%s) failed", url);
189#endif
190 if (furl)
191 free(furl);
192 return;
193 }
194 if (furl)
195 free(furl);
196
197 lines = 0;
198 while ((tls ? client_tls : client).connected() ||
199 (tls ? client_tls : client).available()) {
200 String line = (tls ? client_tls : client).readStringUntil('\n');
201
202#ifdef UPDATE_TRACE
203 syslog.logf(LOG_DEBUG, "%s: read body[%d] \"%s\"", __func__,
204 lines, line.c_str());
205#endif
206
207 switch (lines) {
208 case 0:
209 version = line;
210 break;
211 case 1:
212 bytesize = atoi(line.c_str());
213 break;
214 case 2:
215 md5 = line;
216 break;
217 case 3:
218 rom_url = line;
219 break;
220 default:
221#ifdef UPDATE_TRACE
222 syslog.logf("%s: unexpected line %d: %s\r\n", __func__,
223 lines + 1, line.c_str());
224#endif
225 break;
226 }
227
228 lines++;
229 }
230
231 (tls ? client_tls : client).stop();
232
233 if (version == WIFISTATION_VERSION && !force) {
234 outputf("ERROR OTA server reports version %s, no update "
235 "available\r\n", version.c_str());
236 return;
237 } else if (!do_update) {
238 outputf("OK version %s (%d bytes) available, use AT$UPDATE "
239 "to update\r\n", version.c_str(), bytesize);
240 return;
241 }
242
243 /* doing an update, parse the url read */
244#ifdef UPDATE_TRACE
245 syslog.logf(LOG_DEBUG, "%s: doing update with ROM url \"%s\" size %d",
246 __func__, rom_url.c_str(), bytesize);
247#endif
248 if (!update_http_read_until_body((char *)rom_url.c_str(), bytesize))
249 return;
250
251 outputf("Updating to version %s (%d bytes) from %s\r\n",
252 version.c_str(), bytesize, (char *)rom_url.c_str());
253
254 Update.begin(bytesize, U_FLASH, pRedLED);
255
256 Update.setMD5(md5.c_str());
257
258 Update.onProgress([](unsigned int progress, unsigned int total) {
259 outputf("\rFlash update progress: % 6u of % 6u", progress,
260 total);
261 });
262
263 if ((int)Update.writeStream((tls ? client_tls : client)) != bytesize) {
264 switch (Update.getError()) {
265 case UPDATE_ERROR_BOOTSTRAP:
266 outputf("\nERROR update must be done from fresh "
267 "reset, not from uploaded code\r\n");
268 break;
269 case UPDATE_ERROR_MAGIC_BYTE:
270 outputf("\nERROR image does not start with 0xE9\r\n");
271 break;
272 default:
273 outputf("\nERROR failed writing download bytes: %d\r\n",
274 Update.getError());
275 }
276
277 while ((tls ? client_tls : client).available())
278 (tls ? client_tls : client).read();
279
280 (tls ? client_tls : client).stop();
281 return;
282 }
283
284 if (!Update.end()) {
285 outputf("\nERROR failed update at finish: %d\r\n",
286 Update.getError());
287 return;
288 }
289
290 (tls ? client_tls : client).stop();
291 outputf("\r\nOK update completed, restarting\r\n");
292
293 delay(500);
294 ESP.restart();
295}