From 3cd05291d04995e4ddb5dc410f72508b0cb1dfb5 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Thu, 23 Jun 2022 16:32:42 -0700 Subject: [PATCH] Static files + welcome page --- .../espressif/common-hal/mdns/RemoteService.c | 28 ++++ shared-bindings/mdns/RemoteService.h | 4 + .../web_workflow/static/blinka_16x16.ico | Bin 0 -> 318 bytes .../web_workflow/static}/directory.html | 2 +- .../shared/web_workflow/static/directory.js | 23 ++- .../shared/web_workflow/static/welcome.html | 21 +++ .../shared/web_workflow/static/welcome.js | 62 ++++++++ supervisor/shared/web_workflow/web_workflow.c | 150 ++++++++++++------ supervisor/supervisor.mk | 12 +- tools/gen_web_workflow_static.py | 36 +++++ 10 files changed, 273 insertions(+), 65 deletions(-) create mode 100644 supervisor/shared/web_workflow/static/blinka_16x16.ico rename {tools => supervisor/shared/web_workflow/static}/directory.html (88%) rename tools/circuitpython.js => supervisor/shared/web_workflow/static/directory.js (90%) create mode 100644 supervisor/shared/web_workflow/static/welcome.html create mode 100644 supervisor/shared/web_workflow/static/welcome.js create mode 100644 tools/gen_web_workflow_static.py diff --git a/ports/espressif/common-hal/mdns/RemoteService.c b/ports/espressif/common-hal/mdns/RemoteService.c index 1d80c738bd..a19fe8cc20 100644 --- a/ports/espressif/common-hal/mdns/RemoteService.c +++ b/ports/espressif/common-hal/mdns/RemoteService.c @@ -26,6 +26,8 @@ #include "shared-bindings/mdns/RemoteService.h" +#include "shared-bindings/ipaddress/IPv4Address.h" + const char *common_hal_mdns_remoteservice_get_service_type(mdns_remoteservice_obj_t *self) { if (self->result == NULL) { return ""; @@ -61,6 +63,32 @@ mp_int_t common_hal_mdns_remoteservice_get_port(mdns_remoteservice_obj_t *self) return self->result->port; } +uint32_t mdns_remoteservice_get_ipv4(mdns_remoteservice_obj_t *self) { + if (self->result == NULL || + self->result->ip_protocol != MDNS_IP_PROTOCOL_V4 || + self->result->addr == NULL) { + return 0; + } + mdns_ip_addr_t *cur = self->result->addr; + while (cur != NULL) { + if (cur->addr.type == ESP_IPADDR_TYPE_V4) { + return cur->addr.u_addr.ip4.addr; + } + + cur = cur->next; + } + + return 0; +} + +mp_obj_t common_hal_mdns_remoteservice_get_ipv4(mdns_remoteservice_obj_t *self) { + uint32_t addr = mdns_remoteservice_get_ipv4(self); + if (addr == 0) { + return mp_const_none; + } + return common_hal_ipaddress_new_ipv4address(addr); +} + void common_hal_mdns_remoteservice_deinit(mdns_remoteservice_obj_t *self) { mdns_query_results_free(self->result); self->result = NULL; diff --git a/shared-bindings/mdns/RemoteService.h b/shared-bindings/mdns/RemoteService.h index f751b683ed..5a9d3d5e5b 100644 --- a/shared-bindings/mdns/RemoteService.h +++ b/shared-bindings/mdns/RemoteService.h @@ -38,4 +38,8 @@ const char *common_hal_mdns_remoteservice_get_protocol(mdns_remoteservice_obj_t const char *common_hal_mdns_remoteservice_get_instance_name(mdns_remoteservice_obj_t *self); const char *common_hal_mdns_remoteservice_get_hostname(mdns_remoteservice_obj_t *self); mp_int_t common_hal_mdns_remoteservice_get_port(mdns_remoteservice_obj_t *self); +mp_obj_t common_hal_mdns_remoteservice_get_ipv4(mdns_remoteservice_obj_t *self); void common_hal_mdns_remoteservice_deinit(mdns_remoteservice_obj_t *self); + +// For internal use. +uint32_t mdns_remoteservice_get_ipv4(mdns_remoteservice_obj_t *self); diff --git a/supervisor/shared/web_workflow/static/blinka_16x16.ico b/supervisor/shared/web_workflow/static/blinka_16x16.ico new file mode 100644 index 0000000000000000000000000000000000000000..ff2937dfee32c98d6a34162b96e5d8b4ee8aafe6 GIT binary patch literal 318 zcmZ{fu}T9$6h-gGfMo^?+sIUlrL$d-4Ve+GC01)2wlI}NB3RjM`m7&dXJ?ZEi)q5* zhxiG`N<HYMX~zRyXl#7ft3uVT~Gu!kQNqyfCl5U>@S)=YSSD0G17(^w%NaXQTuC Pb%9L^{7eaRP19`w1vX@( literal 0 HcmV?d00001 diff --git a/tools/directory.html b/supervisor/shared/web_workflow/static/directory.html similarity index 88% rename from tools/directory.html rename to supervisor/shared/web_workflow/static/directory.html index 40aa204757..50d05e815d 100644 --- a/tools/directory.html +++ b/supervisor/shared/web_workflow/static/directory.html @@ -1,4 +1,4 @@ - +

TypeSizePathModified


+🗀  diff --git a/tools/circuitpython.js b/supervisor/shared/web_workflow/static/directory.js similarity index 90% rename from tools/circuitpython.js rename to supervisor/shared/web_workflow/static/directory.js index 73e7caa05a..d053a28ba7 100644 --- a/tools/circuitpython.js +++ b/supervisor/shared/web_workflow/static/directory.js @@ -18,7 +18,6 @@ async function refresh_list() { } ); const data = await response.json(); - console.log(data); var new_children = []; var template = document.querySelector('#row'); @@ -76,12 +75,17 @@ async function refresh_list() { } async function find_devices() { - const response = await fetch("http://circuitpython.local/cp/devices.json"); - let url = new URL("/", response.url); - console.log(response, url); - url_base = url.href; - const data = await response.json(); - console.log(data); + const version_response = await fetch("/cp/version.json"); + if (version_response.ok) { + url_base = new URL("/", window.location).href; + } else { + // TODO: Remove this when we've settled things. It is only used when this file isn't hosted + // by a CP device. + const response = await fetch("http://circuitpython.local/cp/devices.json"); + let url = new URL("/", response.url); + url_base = url.href; + const data = await response.json(); + } refresh_list(); } @@ -103,9 +107,7 @@ async function mkdir(e) { } async function upload(e) { - console.log("upload"); for (const file of files.files) { - console.log(file); let file_path = new URL("/fs" + current_path + file.name, url_base); const response = await fetch(file_path, { @@ -119,7 +121,6 @@ async function upload(e) { ) if (response.ok) { refresh_list(); - console.log(files); files.value = ""; upload_button.disabled = true; } @@ -127,8 +128,6 @@ async function upload(e) { } async function del(e) { - console.log("delete"); - console.log(e); let fn = new URL(e.target.value); var prompt = "Delete " + fn.pathname.substr(3); if (e.target.value.endsWith("/")) { diff --git a/supervisor/shared/web_workflow/static/welcome.html b/supervisor/shared/web_workflow/static/welcome.html new file mode 100644 index 0000000000..941d99df80 --- /dev/null +++ b/supervisor/shared/web_workflow/static/welcome.html @@ -0,0 +1,21 @@ + + + + CircuitPython + + + + +

Welcome!

+ Welcome to CircuitPython's Web API. Through your browser you can work with files. Make sure you've set CIRCUITPY_WEB_API_PASSWORD in /.env and provide it when the browser prompts for it. Leave the username blank. +

Device Info

+ Board:
+ Version:
+ Hostname:
+ IP: +

Other Devices

+ Here are other CircuitPython devices on your network: +
    +
+ + diff --git a/supervisor/shared/web_workflow/static/welcome.js b/supervisor/shared/web_workflow/static/welcome.js new file mode 100644 index 0000000000..79efb7e9a8 --- /dev/null +++ b/supervisor/shared/web_workflow/static/welcome.js @@ -0,0 +1,62 @@ +var url_base = window.location; +var current_path; + +var mdns_works = window.location.hostname.endsWith(".local"); + +async function find_devices() { + var version_response = await fetch("/cp/version.json"); + if (version_response.ok) { + url_base = new URL("/", window.location).href; + } else { + // TODO: Remove this when we've settled things. It is only used when this file isn't hosted + // by a CP device. + version_response = await fetch("http://circuitpython.local/cp/version.json", {mode: "cors"}); + mdns_works = mdns_works || version_response.redirected; + if (!version_response.ok && version_response.redirected) { + version_response = await fetch(version_response.url); + } + let url = new URL("/", version_response.url); + url_base = url.href; + } + const version_info = await version_response.json(); + let version_span = document.querySelector("#version"); + version_span.textContent = version_info.version; + let board_link = document.querySelector("#board"); + board_link.href = "https://circuitpython.org/board/" + version_info.board_id + "/"; + board_link.textContent = version_info.board_name; + let hostname = document.querySelector("#hostname"); + var port = ""; + if (version_info.port != 80) { + port = ":" + version_info.port; + } + hostname.href = "http://" + version_info.hostname + ".local" + port + "/"; + hostname.textContent = version_info.hostname; + let ip = document.querySelector("#ip"); + ip.href = "http://" + version_info.ip + port + "/"; + ip.textContent = version_info.ip; + const response = await fetch(new URL("/cp/devices.json", url_base)); + const data = await response.json(); + let device_list = document.querySelector("#devices"); + let new_devices = []; + for (device of data.devices) { + let li = document.createElement("li"); + let a = document.createElement("a"); + li.appendChild(a); + var port = ""; + if (device.port != 80) { + port = ":" + version_info.port; + } + var server; + if (mdns_works) { + server = device.hostname + ".local"; + } else { + server = device.ip; + } + a.href = "http://" + server + port + "/"; + a.textContent = device.instance_name + " (" + device.hostname + ")"; + new_devices.push(li); + } + device_list.replaceChildren(...new_devices); +} + +find_devices(); diff --git a/supervisor/shared/web_workflow/web_workflow.c b/supervisor/shared/web_workflow/web_workflow.c index 736ff915ee..0895e8b9a5 100644 --- a/supervisor/shared/web_workflow/web_workflow.c +++ b/supervisor/shared/web_workflow/web_workflow.c @@ -250,29 +250,10 @@ void supervisor_start_web_workflow(void) { active.num = -1; active.connected = false; - // Accept a connection and start parsing: - // * HTTP method - // * HTTP path - // * Headers we care about: - // * Authentication - // * Must match CIRCUITPY_WEB_AUTH - // * Host - // * IP - ok - // * cpy-mac.local - ok - // * circuitpython.local - redirect - // * Content-Length - // - // PUT /fs/ - // GET /fs/ - // - File contents - // - JSON directory representation - // GET /cp/devices.json - // - JSON list of MDNS results - // GET /cp/version.json - // - JSON version info + // TODO: // GET /cp/serial.txt // - Most recent 1k of serial output. - // GET / + // GET /edit/ // - Super basic editor // GET /ws/circuitpython // GET /ws/user @@ -313,17 +294,32 @@ static bool _endswith(const char *str, const char *suffix) { return strcmp(str + (strlen(str) - strlen(suffix)), suffix) == 0; } -static void _cors_header(socketpool_socket_obj_t *socket, _request *request) { - bool origin_ok = false; +const char *ok_hosts[] = {"code.circuitpython.org"}; + +static bool _origin_ok(const char *origin) { #if CIRCUITPY_DEBUG - origin_ok = true; + return true; #endif - _send_str(socket, "Access-Control-Allow-Credentials: true\r\nVary: Origin\r\n"); - if (origin_ok) { - _send_str(socket, "Access-Control-Allow-Origin: "); - _send_str(socket, request->origin); - _send_str(socket, "\r\n"); + const char *localhost = "127.0.0.1:"; + const char *http = "http://"; + // This is a prefix check. + if (memcmp(origin + strlen(http), localhost, strlen(localhost)) == 0) { + return true; } + for (size_t i = 0; i < MP_ARRAY_SIZE(ok_hosts); i++) { + // This checks exactly. + if (strcmp(origin + strlen(http), ok_hosts[i]) == 0) { + return true; + } + } + return false; +} + +static void _cors_header(socketpool_socket_obj_t *socket, _request *request) { + _send_str(socket, "Access-Control-Allow-Credentials: true\r\nVary: Origin\r\n"); + _send_str(socket, "Access-Control-Allow-Origin: "); + _send_str(socket, request->origin); + _send_str(socket, "\r\n"); } static void _reply_continue(socketpool_socket_obj_t *socket, _request *request) { @@ -367,6 +363,14 @@ static void _reply_missing(socketpool_socket_obj_t *socket, _request *request) { _send_str(socket, "\r\n"); } +static void _reply_method_not_allowed(socketpool_socket_obj_t *socket, _request *request) { + const char *response = "HTTP/1.1 405 Method Not Allowed\r\n" + "Content-Length: 0\r\n"; + _send_str(socket, response); + _cors_header(socket, request); + _send_str(socket, "\r\n"); +} + static void _reply_forbidden(socketpool_socket_obj_t *socket, _request *request) { const char *response = "HTTP/1.1 403 Forbidden\r\n" "Content-Length: 0\r\n"; @@ -486,22 +490,6 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req _send_chunk(socket, ""); } -static void _reply_directory_html(socketpool_socket_obj_t *socket, _request *request, FF_DIR *dir, const char *request_path, const char *path) { - const char *ok_response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" - "Content-Type: text/html\r\n"; - socketpool_socket_send(socket, (const uint8_t *)ok_response, strlen(ok_response)); - _cors_header(socket, request); - _send_str(socket, "\r\n"); - _send_chunk(socket, ""); - _send_chunk(socket, ""); - _send_chunk(socket, "

"); - _send_chunk(socket, "
TypeSizePathModified

"); - - _send_chunk(socket, "
+🗀 "); - _send_chunk(socket, ""); - _send_chunk(socket, ""); -} - static void _reply_with_file(socketpool_socket_obj_t *socket, _request *request, const char *filename, FIL *active_file) { uint32_t total_length = f_size(active_file); char encoded_len[10]; @@ -577,7 +565,14 @@ static void _reply_with_devices_json(socketpool_socket_obj_t *socket, _request * int port = common_hal_mdns_remoteservice_get_port(&found_devices[i]); snprintf(port_encoded, sizeof(port_encoded), "%d", port); _send_chunk(socket, port_encoded); - _send_chunk(socket, "}"); + _send_chunk(socket, ", \"ip\": \""); + + char ip_encoded[4 * 4]; + uint32_t ipv4_address = mdns_remoteservice_get_ipv4(&found_devices[i]); + uint8_t *octets = (uint8_t *)&ipv4_address; + snprintf(ip_encoded, sizeof(ip_encoded), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]); + _send_chunk(socket, ip_encoded); + _send_chunk(socket, "\"}"); common_hal_mdns_remoteservice_deinit(&found_devices[i]); } _send_chunk(socket, "]}"); @@ -607,7 +602,17 @@ static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request * _send_chunk(socket, ", \"creation_id\": "); snprintf(encoded_id, sizeof(encoded_id), "%d", CIRCUITPY_CREATION_ID); _send_chunk(socket, encoded_id); - _send_chunk(socket, "}"); + _send_chunk(socket, ", \"hostname\": \""); + _send_chunk(socket, common_hal_mdns_server_get_hostname(&mdns)); + _send_chunk(socket, "\", \"port\": 80"); + _send_chunk(socket, ", \"ip\": \""); + + char ip_encoded[4 * 4]; + uint32_t ipv4_address = wifi_radio_get_ipv4_address(&common_hal_wifi_radio_obj); + uint8_t *octets = (uint8_t *)&ipv4_address; + snprintf(ip_encoded, sizeof(ip_encoded), "%d.%d.%d.%d", octets[0], octets[1], octets[2], octets[3]); + _send_chunk(socket, ip_encoded); + _send_chunk(socket, "\"}"); // Empty chunk signals the end of the response. _send_chunk(socket, ""); } @@ -730,9 +735,36 @@ static void _write_file_and_reply(socketpool_socket_obj_t *socket, _request *req } } +#define STATIC_FILE(filename) extern uint32_t filename##_length; extern uint8_t filename[]; extern const char *filename##_content_type; + +STATIC_FILE(directory_html); +STATIC_FILE(directory_js); +STATIC_FILE(welcome_html); +STATIC_FILE(welcome_js); +STATIC_FILE(blinka_16x16_ico); + +static void _reply_static(socketpool_socket_obj_t *socket, _request *request, const uint8_t *response, size_t response_len, const char *content_type) { + uint32_t total_length = response_len; + char encoded_len[10]; + snprintf(encoded_len, sizeof(encoded_len), "%d", total_length); + + _send_str(socket, "HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: "); + _send_str(socket, encoded_len); + _send_str(socket, "\r\n"); + _send_str(socket, "Content-Type: "); + _send_str(socket, content_type); + _send_str(socket, "\r\n"); + _send_str(socket, "\r\n"); + _send_raw(socket, response, response_len); +} + +#define _REPLY_STATIC(socket, request, filename) _reply_static(socket, request, filename, filename##_length, filename##_content_type) + static bool _reply(socketpool_socket_obj_t *socket, _request *request) { if (request->redirect) { _reply_redirect(socket, request, request->path); + } else if (strlen(request->origin) > 0 && !_origin_ok(request->origin)) { + _reply_forbidden(socket, request); } else if (memcmp(request->path, "/fs/", 4) == 0) { if (!request->authenticated) { ESP_LOGW(TAG, "not authed"); @@ -800,8 +832,10 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) { } if (request->json) { _reply_directory_json(socket, request, &dir, request->path, path); + } else if (pathlen == 1) { + _REPLY_STATIC(socket, request, directory_html); } else { - _reply_directory_html(socket, request, &dir, request->path, path); + _reply_missing(socket, request); } f_closedir(&dir); @@ -857,10 +891,24 @@ static bool _reply(socketpool_socket_obj_t *socket, _request *request) { } else if (strcmp(path, "/version.json") == 0) { _reply_with_version_json(socket, request); } + } else if (strcmp(request->method, "GET") != 0) { + _reply_method_not_allowed(socket, request); } else { - const char *ok_response = "HTTP/1.1 200 OK\r\nContent-Length: 11\r\nContent-Type: text/plain\r\n\r\nHello World"; - int sent = socketpool_socket_send(socket, (const uint8_t *)ok_response, strlen(ok_response)); - ESP_LOGW(TAG, "sent ok %d", sent); + if (strcmp(request->path, "/") == 0) { + // TODO: Autogenerate this based on the blinka bitmap and change the + // palette based on MAC address. + _REPLY_STATIC(socket, request, welcome_html); + } else if (strcmp(request->path, "/directory.js") == 0) { + _REPLY_STATIC(socket, request, directory_js); + } else if (strcmp(request->path, "/welcome.js") == 0) { + _REPLY_STATIC(socket, request, welcome_js); + } else if (strcmp(request->path, "/favicon.ico") == 0) { + // TODO: Autogenerate this based on the blinka bitmap and change the + // palette based on MAC address. + _REPLY_STATIC(socket, request, blinka_16x16_ico); + } else { + _reply_missing(socket, request); + } } return false; } diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk index 99088bc79a..e420472c14 100644 --- a/supervisor/supervisor.mk +++ b/supervisor/supervisor.mk @@ -159,8 +159,18 @@ ifeq ($(CIRCUITPY_USB),1) endif endif +STATIC_RESOURCES = $(wildcard $(TOP)/supervisor/shared/web_workflow/static/*) + +$(BUILD)/autogen_web_workflow_static.c: ../../tools/gen_web_workflow_static.py $(STATIC_RESOURCES) + $(STEPECHO) "GEN $@" + $(Q)$(PYTHON) $< \ + --output_c_file $@ \ + $(STATIC_RESOURCES) + ifeq ($(CIRCUITPY_WEB_WORKFLOW),1) SRC_SUPERVISOR += supervisor/shared/web_workflow/web_workflow.c + + SRC_SUPERVISOR += $(BUILD)/autogen_web_workflow_static.c endif SRC_TINYUSB = $(filter lib/tinyusb/%.c, $(SRC_SUPERVISOR)) @@ -217,4 +227,4 @@ $(BUILD)/autogen_display_resources-$(TRANSLATION).c: ../../tools/gen_display_res $(Q)$(PYTHON) ../../tools/gen_display_resources.py \ --font $(CIRCUITPY_DISPLAY_FONT) \ --sample_file $(TOP)/locale/$(TRANSLATION).po \ - --output_c_file $(BUILD)/autogen_display_resources-$(TRANSLATION).c + --output_c_file $@ diff --git a/tools/gen_web_workflow_static.py b/tools/gen_web_workflow_static.py new file mode 100644 index 0000000000..5f0febd084 --- /dev/null +++ b/tools/gen_web_workflow_static.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2014 MicroPython & CircuitPython contributors (https://github.com/adafruit/circuitpython/graphs/contributors) +# +# SPDX-License-Identifier: MIT + +import argparse +import gzip +import mimetypes +import pathlib + +parser = argparse.ArgumentParser(description="Generate displayio resources.") +parser.add_argument("--output_c_file", type=argparse.FileType("w"), required=True) +parser.add_argument("files", metavar="FILE", type=argparse.FileType("rb"), nargs="+") + +args = parser.parse_args() + + +c_file = args.output_c_file + +c_file.write(f"// Autogenerated by tools/gen_web_workflow_static.py\n") +c_file.write(f"#include \n\n") + +for f in args.files: + path = pathlib.Path(f.name) + variable = path.name.replace(".", "_") + uncompressed = f.read() + ulen = len(uncompressed) + compressed = gzip.compress(uncompressed) + clen = len(compressed) + compressed = ", ".join([hex(x) for x in compressed]) + mime = mimetypes.guess_type(f.name)[0] + + c_file.write(f"// {f.name}\n") + c_file.write(f"// Original length: {ulen} Compressed length: {clen}\n") + c_file.write(f"const uint32_t {variable}_length = {clen};\n") + c_file.write(f'const char* {variable}_content_type = "{mime}";\n') + c_file.write(f"const uint8_t {variable}[{clen}] = {{{compressed}}};\n\n")