ESP8266-based WiFi serial modem emulator ROM
1/*
2 * WiFiPPP
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 "wifippp.h"
21
22static const char OTA_VERSION_URL[] PROGMEM =
23 "https://raw.githubusercontent.com/jcs/WiFiPPP/main/release/version.txt";
24
25static WiFiClient client;
26static WiFiClientSecure client_tls;
27static bool tls = false;
28
29bool
30update_https_get_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 host = (char *)malloc(strlen(url) + 1);
41 if (host == NULL) {
42 output("ERROR malloc failed\r\n");
43 return false;
44 }
45
46 path = (char *)malloc(strlen(url) + 1);
47 if (path == NULL) {
48 output("ERROR malloc failed\r\n");
49 free(host);
50 return false;
51 }
52
53 if (sscanf(url, "http://%[^/]%s%n", host, path, &chars) == 2 &&
54 chars != 0) {
55 tls = false;
56 } else if (sscanf(url, "https://%[^/]%s%n", host, path, &chars) == 2
57 && chars != 0) {
58 tls = true;
59 /*
60 * This would be nice to not have to do, but keeping up with
61 * GitHub's TLS cert fingerprints will be tedious, and we have
62 * no cert chain.
63 */
64 client_tls.setInsecure();
65 } else {
66 outputf("ERROR failed parsing URL \"%s\"\r\n", url);
67 free(path);
68 free(host);
69 return false;
70 }
71
72 if ((colon = strchr(host, ':'))) {
73 colon[0] = '\0';
74 port = atoi(colon + 1);
75 } else {
76 port = (tls ? 443 : 80);
77 }
78
79#ifdef UPDATE_TRACE
80 syslog.logf(LOG_DEBUG, "%s: host \"%s\" path \"%s\" port %d tls %d",
81 __func__, host, path, port, tls ? 1 : 0);
82#endif
83
84 if (!(tls ? client_tls : client).connect(host, port)) {
85 outputf("ERROR OTA failed connecting to http%s://%s:%d\r\n",
86 tls ? "s" : "", host, port);
87 free(path);
88 free(host);
89 return false;
90 }
91
92 (tls ? client_tls : client).printf("GET %s HTTP/1.0\r\n", path);
93 (tls ? client_tls : client).printf("Host: %s\r\n", host);
94 (tls ? client_tls : client).printf("User-Agent: WiFiPPP %s\r\n", WIFIPPP_VERSION);
95 (tls ? client_tls : client).printf("Connection: close\r\n");
96 (tls ? client_tls : client).printf("\r\n");
97
98 free(path);
99 free(host);
100
101 /* read headers */
102 lines = 0;
103 while ((tls ? client_tls : client).connected() ||
104 (tls ? client_tls : client).available()) {
105 String line = (tls ? client_tls : client).readStringUntil('\n');
106
107#ifdef UPDATE_TRACE
108 syslog.logf(LOG_DEBUG, "%s: read header \"%s\"", __func__,
109 line.c_str());
110#endif
111
112 if (lines == 0)
113 sscanf(line.c_str(), "HTTP/1.%d %d%n", &httpver,
114 &status, &chars);
115 else if (sscanf(line.c_str(), "Content-Length: %d%n",
116 &tlength, &chars) == 1 && chars > 0)
117 clength = tlength;
118 else if (line == "\r")
119 break;
120
121 lines++;
122 }
123
124#ifdef UPDATE_TRACE
125 syslog.logf(LOG_DEBUG, "%s: read status %d, content-length %d vs "
126 "expected %ld, finished on line %d", __func__, status, clength,
127 expected_length, lines);
128#endif
129
130 if (status != 200) {
131 outputf("ERROR OTA fetch of %s failed with HTTP status %d\r\n",
132 url, status);
133 goto drain;
134 }
135
136 if (expected_length != 0 && clength != expected_length) {
137 outputf("ERROR OTA fetch of %s expected to be size %d, "
138 "fetched %d\r\n", url, expected_length, clength);
139 goto drain;
140 }
141
142 return true;
143
144drain:
145#ifdef UPDATE_TRACE
146 syslog.logf(LOG_DEBUG, "%s: draining remaining body", __func__);
147#endif
148 while ((tls ? client_tls : client).available())
149 (tls ? client_tls : client).read();
150 (tls ? client_tls : client).stop();
151 return false;
152}
153
154void
155update_process(char *url, bool do_update, bool force)
156{
157 String rom_url, md5, version;
158 int bytesize = 0, lines = 0, len;
159 char *furl = NULL;
160
161 outputf("\n");
162
163 if (url == NULL) {
164 furl = url = (char *)malloc(len = (strlen_P(OTA_VERSION_URL) +
165 1));
166 if (url == NULL) {
167 output("ERROR malloc failed\r\n");
168 return;
169 }
170 memcpy_P(url, OTA_VERSION_URL, len);
171 }
172
173#ifdef UPDATE_TRACE
174 syslog.logf(LOG_DEBUG, "fetching update manifest from \"%s\"", url);
175#endif
176
177 if (!update_https_get_body(url, 0)) {
178#ifdef UPDATE_TRACE
179 syslog.logf(LOG_DEBUG, "update_https_get_body(%s) failed", url);
180#endif
181 if (furl)
182 free(furl);
183 return;
184 }
185 if (furl)
186 free(furl);
187
188#ifdef UPDATE_TRACE
189 syslog.logf(LOG_DEBUG, "reading body of manifest (available %d)",
190 (tls ? client_tls : client).available());
191#endif
192
193 lines = 0;
194 while ((tls ? client_tls : client).connected() ||
195 (tls ? client_tls : client).available()) {
196 String line = (tls ? client_tls : client).readStringUntil('\n');
197
198#ifdef UPDATE_TRACE
199 syslog.logf(LOG_DEBUG, "%s: read body[%d] \"%s\"", __func__,
200 lines, line.c_str());
201#endif
202
203 switch (lines) {
204 case 0:
205 version = line;
206 break;
207 case 1:
208 bytesize = atoi(line.c_str());
209 break;
210 case 2:
211 md5 = line;
212 break;
213 case 3:
214 rom_url = line;
215 break;
216 default:
217#ifdef UPDATE_TRACE
218 syslog.logf("%s: unexpected line %d: %s\r\n", __func__,
219 lines + 1, line.c_str());
220#endif
221 break;
222 }
223
224 lines++;
225 }
226
227 (tls ? client_tls : client).stop();
228
229#ifdef UPDATE_TRACE
230 syslog.logf(LOG_DEBUG, "done reading body after %d lines", lines);
231#endif
232
233 if (version == WIFIPPP_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_https_get_body((char *)rom_url.c_str(), bytesize))
249 return;
250
251 state = STATE_UPDATING;
252 pixel_color_by_state();
253
254 outputf("Updating to version %s (%d bytes) from %s\r\n",
255 version.c_str(), bytesize, (char *)rom_url.c_str());
256
257 Update.begin(bytesize, U_FLASH, -1);
258
259 Update.setMD5(md5.c_str());
260
261 Update.onProgress([](unsigned int progress, unsigned int total) {
262 /*
263 * Just force serial output here rather than using outputf,
264 * don't let it block us.
265 */
266 if (progress == 1 || progress % 1024 == 0 || progress == total)
267 Serial.printf("\rFlash update progress:% 7d of %d",
268 progress, total);
269 });
270
271 if ((int)Update.writeStream((tls ? client_tls : client)) != bytesize) {
272 if (Update.getError() == UPDATE_ERROR_BOOTSTRAP)
273 outputf("ERROR update must be done from fresh "
274 "reset, not from uploaded code\r\n");
275 else
276 outputf("ERROR failed writing download bytes: %d\r\n",
277 Update.getError());
278
279 while ((tls ? client_tls : client).available())
280 (tls ? client_tls : client).read();
281
282 (tls ? client_tls : client).stop();
283 return;
284 }
285
286 if (!Update.end()) {
287 outputf("ERROR failed update at finish: %d\r\n",
288 Update.getError());
289 return;
290 }
291
292 (tls ? client_tls : client).stop();
293 outputf("\r\nOK update completed, restarting\r\n");
294
295 delay(500);
296 ESP.restart();
297}