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 <ESP8266WebServer.h>
19#include "wifistation.h"
20
21std::unique_ptr<ESP8266WebServer> http = NULL;
22
23static const char html_wrapper[] PROGMEM = R"END(<!doctype html>
24<html>
25<head>
26 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
27 <title>WiFiStation at %s</title>
28 <style type="text/css">
29 body {
30 background-color: #fffff8;
31 font-family: sans-serif;
32 font-size: 12pt;
33 padding: 1em;
34 }
35 h3 {
36 margin: 0;
37 }
38 </style>
39</head>
40<body>
41<h3>WiFiStation %s</h3>
42<p>
43%s
44</body>
45</html>
46)END";
47
48static unsigned int upload_size = 0;
49static unsigned int delivered_bytes = 0;
50
51void
52http_process(void)
53{
54 if (!settings->http_server)
55 return;
56
57 http->handleClient();
58}
59
60void
61http_send_result(int status, bool include_home, const char *body, ...)
62{
63 va_list arg;
64 const char *accept = http->header("Accept").c_str();
65 int len;
66 char *doc = NULL;
67
68 /* expand body format */
69 va_start(arg, body);
70 len = vasprintf(&doc, body, arg);
71 va_end(arg);
72
73 if (len == -1)
74 goto fail;
75
76 /* if Accept header starts with text/html, client prefers html */
77 if (accept && strncmp(accept, "text/html", 9) == 0) {
78 char *tmp;
79
80 /* append home link to body */
81 if (include_home) {
82 len = asprintf(&tmp, "%s"
83 "<p>"
84 "<a href=\"/\">Home</a>", doc);
85 if (len == -1)
86 goto fail;
87 free(doc);
88 doc = tmp;
89 }
90
91 /* insert body into html template */
92 len = asprintf(&tmp, html_wrapper,
93 WiFi.localIP().toString().c_str(), WIFISTATION_VERSION,
94 doc);
95 if (len == -1)
96 goto fail;
97 free(doc);
98 doc = tmp;
99
100 http->send(status, "text/html", doc);
101 } else {
102 /* append newline since this is probably going to a terminal */
103 doc = (char *)realloc(doc, len + 2);
104 doc[len] = '\n';
105 doc[len + 1] = '\0';
106 http->send(status, "text/plain", doc);
107 }
108
109 free(doc);
110 return;
111
112fail:
113 if (doc != NULL)
114 free(doc);
115 http->send(500, "text/plain", "out of memory :(");
116 return;
117}
118
119void
120http_setup(void)
121{
122 if (settings->http_server) {
123 if (http)
124 return;
125
126 http.reset(new ESP8266WebServer(80));
127 } else {
128 if (http)
129 http = NULL;
130
131 return;
132 }
133
134 http->collectHeaders(F("Accept"));
135
136 http->on("/", HTTP_GET, []() {
137 http_send_result(200, false, R"END(
138<form action="/upload" method="POST" enctype="multipart/form-data">
139<p>
140Binary to execute on MailStation (<em>maximum size is %d bytes</em>):
141<p>
142<input type="file" name="file" maxlength="%d">
143<br>
144<p>
145Ensure the WSLoader application is running on the MailStation ready to accept
146the upload.
147<p>
148<input type="submit" value="Upload">
149</form>
150)END",
151 MAX_UPLOAD_SIZE, MAX_UPLOAD_SIZE);
152 });
153
154 /*
155 * This measure step is because we don't get the total size of the
156 * upload until we read the whole thing, but we need to send the file
157 * size to the MailStation before sending any data. So to avoid
158 * caching the entire upload, we process the upload and just count the
159 * bytes being passed through, then send a 307 redirect to the actual
160 * upload endpoint with the total size. Browsers follow a 307 with a
161 * POST, so at /upload we'll have the actual size before it sends us
162 * the same data.
163 */
164 http->on("/upload", HTTP_POST, []() {
165 char tmp[32];
166
167 if (upload_size == 0) {
168 http_send_result(400, true,
169 "Failed receiving file (size 0).");
170 } else {
171 snprintf(tmp, sizeof(tmp), "/upload_measured?size=%d",
172 upload_size);
173 http->sendHeader("Location", tmp);
174 http->send(307, "text/plain", "Ok, follow me!");
175 }
176 }, []() {
177 HTTPUpload& upload = http->upload();
178
179 switch (upload.status) {
180 case UPLOAD_FILE_START:
181 upload_size = 0;
182 break;
183 case UPLOAD_FILE_WRITE:
184 upload_size += upload.currentSize;
185 break;
186 case UPLOAD_FILE_ABORTED:
187 upload_size = 0;
188 /* FALLTHROUGH */
189 case UPLOAD_FILE_END:
190 if (upload_size == 0) {
191 http_send_result(400, true,
192 "Failed receiving file (size 0).");
193 return;
194 }
195
196 if (upload_size > MAX_UPLOAD_SIZE) {
197 http_send_result(400, true,
198 "File upload cannot be larger than %d "
199 "bytes.", MAX_UPLOAD_SIZE);
200 upload_size = 0;
201 return;
202 }
203
204 break;
205 }
206 });
207
208 http->on("/upload_measured", HTTP_POST, []() {
209 http_send_result(200, true,
210 "Successfully uploaded %d byte%s to MailStation.",
211 delivered_bytes, delivered_bytes == 1 ? "" : "s");
212 }, []() {
213 HTTPUpload& upload = http->upload();
214
215 switch (upload.status) {
216 case UPLOAD_FILE_START:
217 delivered_bytes = 0;
218
219 if (!(upload_size = atoi(http->arg("size").c_str()))) {
220 http_send_result(400, true, "No size "
221 "parameter passed. Perhaps your browser "
222 "failed to follow the 307 redirect "
223 "properly.");
224 return;
225 }
226
227 if (upload_size > MAX_UPLOAD_SIZE) {
228 http_send_result(400, true,
229 "File upload cannot be larger than %d "
230 "bytes.", MAX_UPLOAD_SIZE);
231 return;
232 }
233
234 if (ms_write(upload_size & 0xff) != 0 ||
235 ms_write((upload_size >> 8) & 0xff) != 0) {
236 http_send_result(400, true,
237 "Failed sending size bytes "
238 "(<tt>0x%x</tt>, <tt>0x%x</tt>) to "
239 "MailStation. Is the WSLoader program "
240 "running?",
241 upload_size & 0xff,
242 (upload_size >> 8) & 0xff);
243 return;
244 }
245 break;
246 case UPLOAD_FILE_WRITE:
247 for (int i = 0; i < (int)upload.currentSize; i++) {
248 delivered_bytes++;
249 if (ms_write(upload.buf[i]) == -1) {
250 http_send_result(400, true,
251 "Failed uploading to MailStation "
252 "at byte %d/%d.",
253 delivered_bytes, upload_size);
254 return;
255 }
256 yield();
257 delayMicroseconds(500);
258 }
259 break;
260 case UPLOAD_FILE_END:
261 break;
262 case UPLOAD_FILE_ABORTED:
263 http_send_result(400, true,
264 "Aborted upload to MailStation.");
265 return;
266 }
267 });
268
269 http->onNotFound([]() {
270 http_send_result(404, true, "404");
271 });
272
273 http->begin();
274}