Static files + welcome page

This commit is contained in:
Scott Shawcroft 2022-06-23 16:32:42 -07:00
parent 7543dd9af0
commit 3cd05291d0
No known key found for this signature in database
GPG Key ID: 0DFD512649C052DA
10 changed files with 273 additions and 65 deletions

View File

@ -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;

View File

@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -1,4 +1,4 @@
<!DOCTYPE html><html><head><title></title><meta charset="UTF-8"></head>
<script src="http://127.0.0.1:8000/circuitpython.js" defer=true></script>
<script src="/directory.js" defer=true></script>
<body><h1></h1><template id="row"><tr><td></td><td></td><td><a></a></td><td></td><td><button class="delete">🗑️</button></td></tr></template><table><thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th></th></tr></thead><tbody></tbody></table><hr><input type="file" id="files" multiple><button type="submit" id="upload">Upload</button><hr>+🗀&nbsp;<input type="text" id="name"><button type="submit" id="mkdir">Create Directory</button>
</body></html>

View File

@ -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("/")) {

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>CircuitPython</title>
<meta charset="UTF-8">
</head>
<script src="/welcome.js" defer=true></script>
<body>
<h1>Welcome!</h1>
Welcome to CircuitPython's Web API. Through your browser you can <a href="/fs/">work with files</a>. Make sure you've set <code>CIRCUITPY_WEB_API_PASSWORD</code> in <code>/.env</code> and provide it when the browser prompts for it. Leave the username blank.
<h2>Device Info</h2>
Board: <a id="board"></a><br>
Version: <span id="version"></span><br>
Hostname: <a id="hostname"></a><br>
IP: <a id="ip"></a>
<h2>Other Devices</h2>
Here are other CircuitPython devices on your network:
<ul id="devices">
</ul>
</body>
</html>

View File

@ -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();

View File

@ -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/<filename>
// GET /fs/<filename>
// - 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, "<!DOCTYPE html><html><head><title></title><meta charset=\"UTF-8\"></head>");
_send_chunk(socket, "<script src=\"http://127.0.0.1:8000/circuitpython.js\" defer=true></script>");
_send_chunk(socket, "<body><h1></h1><template id=\"row\"><tr><td></td><td></td><td><a></a></td><td></td><td><button class=\"delete\">🗑️</button></td></tr></template><table><thead><tr><th>Type</th><th>Size</th><th>Path</th><th>Modified</th><th></th></tr></thead><tbody>");
_send_chunk(socket, "</tbody></table><hr><input type=\"file\" id=\"files\" multiple><button type=\"submit\" id=\"upload\">Upload</button>");
_send_chunk(socket, "<hr>+🗀&nbsp;<input type=\"text\" id=\"name\"><button type=\"submit\" id=\"mkdir\">Create Directory</button>");
_send_chunk(socket, "</body></html>");
_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;
}

View File

@ -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 $@

View File

@ -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 <stdint.h>\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")