Doc tweak and add vararg send helpers

This commit is contained in:
Scott Shawcroft 2022-06-30 16:37:51 -07:00
parent 3be3e89d82
commit 95d3289df6
No known key found for this signature in database
GPG Key ID: 0DFD512649C052DA
3 changed files with 152 additions and 115 deletions

View File

@ -70,6 +70,17 @@ The web workflow is depends on adding Wi-Fi credentials into the `/.env` file. T
automatically connect to the network and start the webserver used for the workflow. The webserver
is on port 80. It also enables MDNS.
Here is an example `/.env`:
```bash
# To auto-connect to Wi-Fi
CIRCUITPY_WIFI_SSID='scottswifi'
CIRCUITPY_WIFI_PASSWORD='secretpassword'
# To enable modifying files from the web. Change this too!
CIRCUITPY_WEB_API_PASSWORD='passw0rd'
```
MDNS is used to resolve [`circuitpython.local`](http://circuitpython.local) to a device specific
hostname of the form `cpy-XXXXXX.local`. The `XXXXXX` is based on network MAC address. The device
also provides the MDNS service with service type `_circuitpython` and protocol `_tcp`.

View File

@ -7,7 +7,7 @@
<script src="/welcome.js" defer=true></script>
<body>
<h1><a href="/"><img src="/favicon.ico"/></a>&nbsp;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.
Welcome to CircuitPython's Web API. Go to the <a href="/fs/">file browser</a> to work with files in the CIRCUITPY drive. Make sure you've set <code>CIRCUITPY_WEB_API_PASSWORD='somepassword'</code> in <code>/.env</code>. Provide the password 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>

View File

@ -281,6 +281,19 @@ static void _send_str(socketpool_socket_obj_t *socket, const char *str) {
_send_raw(socket, (const uint8_t *)str, strlen(str));
}
// The last argument must be NULL! Otherwise, it won't stop.
static void _send_strs(socketpool_socket_obj_t *socket, ...) {
va_list ap;
va_start(ap, socket);
const char *str = va_arg(ap, const char *);
while (str != NULL) {
_send_str(socket, str);
str = va_arg(ap, const char *);
}
va_end(ap);
}
static void _send_chunk(socketpool_socket_obj_t *socket, const char *chunk) {
char encoded_len[sizeof(size_t) * 2 + 1];
int len = snprintf(encoded_len, sizeof(encoded_len), "%X", strlen(chunk));
@ -290,6 +303,37 @@ static void _send_chunk(socketpool_socket_obj_t *socket, const char *chunk) {
_send_raw(socket, (const uint8_t *)"\r\n", 2);
}
// A bit of a misnomer because it sends all arguments as one chunk.
// The last argument must be NULL! Otherwise, it won't stop.
static void _send_chunks(socketpool_socket_obj_t *socket, ...) {
va_list strs_to_count;
va_start(strs_to_count, socket);
va_list strs_to_send;
va_copy(strs_to_send, strs_to_count);
size_t chunk_len = 0;
const char *str = va_arg(strs_to_count, const char *);
while (str != NULL) {
chunk_len += strlen(str);
str = va_arg(strs_to_count, const char *);
}
va_end(strs_to_count);
char encoded_len[sizeof(size_t) * 2 + 1];
snprintf(encoded_len, sizeof(encoded_len), "%X", chunk_len);
_send_strs(socket, encoded_len, "\r\n", NULL);
str = va_arg(strs_to_send, const char *);
while (str != NULL) {
_send_str(socket, str);
str = va_arg(strs_to_send, const char *);
}
va_end(strs_to_send);
_send_str(socket, "\r\n");
}
static bool _endswith(const char *str, const char *suffix) {
if (str == NULL || suffix == NULL) {
return false;
@ -348,44 +392,46 @@ STATIC bool _usb_active(void) {
return false;
}
static const char *OK_JSON = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: application/json\r\n";
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");
_send_strs(socket,
"Access-Control-Allow-Credentials: true\r\n",
"Vary: Origin\r\n",
"Access-Control-Allow-Origin: ", request->origin, "\r\n",
NULL);
}
static void _reply_continue(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 100 Continue\r\n";
_send_str(socket, response);
_send_str(socket, "HTTP/1.1 100 Continue\r\n");
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_created(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 201 Created\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 201 Created\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_no_content(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 204 No Content\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 204 No Content\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_access_control(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 204 No Content\r\n"
"Content-Length: 0\r\n"
"Access-Control-Expose-Headers: Access-Control-Allow-Methods\r\n"
"Access-Control-Allow-Headers: X-Timestamp, Content-Type\r\n"
"Access-Control-Allow-Methods:GET, OPTIONS";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 204 No Content\r\n",
"Content-Length: 0\r\n",
"Access-Control-Expose-Headers: Access-Control-Allow-Methods\r\n",
"Access-Control-Allow-Headers: X-Timestamp, Content-Type\r\n",
"Access-Control-Allow-Methods:GET, OPTIONS", NULL);
if (!_usb_active()) {
_send_str(socket, ", PUT, DELETE");
}
@ -398,89 +444,85 @@ static void _reply_access_control(socketpool_socket_obj_t *socket, _request *req
}
static void _reply_missing(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 404 Not Found\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 404 Not Found\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, 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);
_send_strs(socket,
"HTTP/1.1 405 Method Not Allowed\r\n",
"Content-Length: 0\r\n", NULL);
_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";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 403 Forbidden\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_conflict(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 409 Conflict\r\n"
"Content-Length: 19\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 409 Conflict\r\n",
"Content-Length: 19\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\nUSB storage active.");
}
static void _reply_payload_too_large(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 413 Payload Too Large\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 413 Payload Too Large\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_expectation_failed(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 417 Expectation Failed\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 417 Expectation Failed\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_unauthorized(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 401 Unauthorized\r\n"
"Content-Length: 0\r\n"
"WWW-Authenticate: Basic realm=\"CircuitPython\"\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 401 Unauthorized\r\n",
"Content-Length: 0\r\n",
"WWW-Authenticate: Basic realm=\"CircuitPython\"\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_server_error(socketpool_socket_obj_t *socket, _request *request) {
const char *response = "HTTP/1.1 500 Internal Server Error\r\n"
"Content-Length: 0\r\n";
_send_str(socket, response);
_send_strs(socket,
"HTTP/1.1 500 Internal Server Error\r\n",
"Content-Length: 0\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_redirect(socketpool_socket_obj_t *socket, _request *request, const char *path) {
const char *redirect_response = "HTTP/1.1 301 Moved Permanently\r\nConnection: close\r\nContent-Length: 0\r\nLocation: http://";
int nodelay = 1;
lwip_setsockopt(socket->num, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
_send_str(socket, redirect_response);
_send_str(socket, common_hal_mdns_server_get_hostname(&mdns));
_send_str(socket, ".local");
_send_str(socket, path);
_send_str(socket, "\r\n");
const char *hostname = common_hal_mdns_server_get_hostname(&mdns);
_send_strs(socket,
"HTTP/1.1 301 Moved Permanently\r\n",
"Connection: close\r\n",
"Content-Length: 0\r\n",
"Location: http://", hostname, ".local", path, "\r\n", NULL);
_cors_header(socket, request);
_send_str(socket, "\r\n");
}
static void _reply_directory_json(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: application/json\r\n";
socketpool_socket_send(socket, (const uint8_t *)ok_response, strlen(ok_response));
socketpool_socket_send(socket, (const uint8_t *)OK_JSON, strlen(OK_JSON));
_cors_header(socket, request);
_send_str(socket, "\r\n");
_send_chunk(socket, "[");
@ -493,9 +535,9 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
if (!first) {
_send_chunk(socket, ",");
}
_send_chunk(socket, "{\"name\": \"");
_send_chunk(socket, file_info.fname);
_send_chunk(socket, "\", \"directory\": ");
_send_chunks(socket,
"{\"name\": \"", file_info.fname, "\",",
"\"directory\": ", NULL);
if ((file_info.fattrib & AM_DIR) != 0) {
_send_chunk(socket, "true");
} else {
@ -514,17 +556,13 @@ static void _reply_directory_json(socketpool_socket_obj_t *socket, _request *req
char encoded_number[32];
snprintf(encoded_number, sizeof(encoded_number), "%lld", truncated_time);
_send_chunk(socket, encoded_number);
_send_chunk(socket, ", \"file_size\": ");
_send_chunks(socket, encoded_number, ", \"file_size\": ", NULL);
size_t file_size = 0;
if ((file_info.fattrib & AM_DIR) == 0) {
file_size = file_info.fsize;
}
snprintf(encoded_number, sizeof(encoded_number), "%d", file_size);
_send_chunk(socket, encoded_number);
_send_chunk(socket, "}");
_send_chunks(socket, encoded_number, "}", NULL);
first = false;
res = f_readdir(dir, &file_info);
}
@ -537,9 +575,10 @@ static void _reply_with_file(socketpool_socket_obj_t *socket, _request *request,
char encoded_len[10];
snprintf(encoded_len, sizeof(encoded_len), "%d", total_length);
_send_str(socket, "HTTP/1.1 200 OK\r\nContent-Length: ");
_send_str(socket, encoded_len);
_send_str(socket, "\r\n");
_send_strs(socket,
"HTTP/1.1 200 OK\r\n",
"Content-Length: ", encoded_len, "\r\n", NULL);
// TODO: Make this a table to save space.
if (_endswith(filename, ".txt") || _endswith(filename, ".py")) {
_send_str(socket, "Content-Type: text/plain\r\n");
} else if (_endswith(filename, ".js")) {
@ -583,36 +622,30 @@ static void _reply_with_devices_json(socketpool_socket_obj_t *socket, _request *
mdns_remoteservice_obj_t found_devices[32];
size_t total_results = mdns_server_find(&mdns, "_circuitpython", "_tcp", 1, found_devices, MP_ARRAY_SIZE(found_devices));
size_t count = MIN(total_results, MP_ARRAY_SIZE(found_devices));
const char *ok_response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: application/json\r\n";
socketpool_socket_send(socket, (const uint8_t *)ok_response, strlen(ok_response));
socketpool_socket_send(socket, (const uint8_t *)OK_JSON, strlen(OK_JSON));
_cors_header(socket, request);
_send_str(socket, "\r\n");
_send_chunk(socket, "{\"total\": ");
char total_encoded[4];
snprintf(total_encoded, sizeof(total_encoded), "%d", total_results);
_send_chunk(socket, total_encoded);
_send_chunk(socket, ", \"devices\": [");
_send_chunks(socket, "{\"total\": ", total_encoded, ", \"devices\": [", NULL);
for (size_t i = 0; i < count; i++) {
if (i > 0) {
_send_chunk(socket, ",");
}
_send_chunk(socket, "{\"hostname\": \"");
_send_chunk(socket, common_hal_mdns_remoteservice_get_hostname(&found_devices[i]));
_send_chunk(socket, "\", \"instance_name\": \"");
_send_chunk(socket, common_hal_mdns_remoteservice_get_instance_name(&found_devices[i]));
_send_chunk(socket, "\", \"port\": ");
const char *hostname = common_hal_mdns_remoteservice_get_hostname(&found_devices[i]);
const char *instance_name = common_hal_mdns_remoteservice_get_instance_name(&found_devices[i]);
char port_encoded[4];
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, ", \"ip\": \"");
char ip_encoded[4 * 4];
uint32_t ipv4_address = mdns_remoteservice_get_ipv4_address(&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, "\"}");
_send_chunks(socket,
"{\"hostname\": \"", hostname, "\", ",
"\"instance_name\": \"", instance_name, "\", ",
"\"port\": ", port_encoded, ", ",
"\"ip\": \"", ip_encoded, "\"}", NULL);
common_hal_mdns_remoteservice_deinit(&found_devices[i]);
}
_send_chunk(socket, "]}");
@ -621,33 +654,27 @@ static void _reply_with_devices_json(socketpool_socket_obj_t *socket, _request *
}
static void _reply_with_version_json(socketpool_socket_obj_t *socket, _request *request) {
const char *ok_response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: application/json\r\n";
socketpool_socket_send(socket, (const uint8_t *)ok_response, strlen(ok_response));
_send_str(socket, OK_JSON);
_cors_header(socket, request);
_send_str(socket, "\r\n");
_send_chunk(socket, "{\"web_api_version\": 1, \"version\": \"");
_send_chunk(socket, MICROPY_GIT_TAG);
_send_chunk(socket, "\", \"build_date\": \"");
_send_chunk(socket, MICROPY_BUILD_DATE);
_send_chunk(socket, "\", \"board_name\": \"");
_send_chunk(socket, MICROPY_HW_BOARD_NAME);
_send_chunk(socket, "\", \"mcu_name\": \"");
_send_chunk(socket, MICROPY_HW_MCU_NAME);
_send_chunk(socket, "\", \"board_id\": \"");
_send_chunk(socket, CIRCUITPY_BOARD_ID);
_send_chunk(socket, "\", \"creator_id\": ");
char encoded_id[11]; // 2 ** 32 is 10 decimal digits plus one for \0
snprintf(encoded_id, sizeof(encoded_id), "%u", CIRCUITPY_CREATOR_ID);
_send_chunk(socket, encoded_id);
_send_chunk(socket, ", \"creation_id\": ");
snprintf(encoded_id, sizeof(encoded_id), "%u", CIRCUITPY_CREATION_ID);
_send_chunk(socket, encoded_id);
_send_chunk(socket, ", \"hostname\": \"");
_send_chunk(socket, common_hal_mdns_server_get_hostname(&mdns));
_send_chunk(socket, "\", \"port\": 80");
_send_chunk(socket, ", \"ip\": \"");
_send_chunk(socket, _our_ip_encoded);
_send_chunk(socket, "\"}");
char encoded_creator_id[11]; // 2 ** 32 is 10 decimal digits plus one for \0
snprintf(encoded_creator_id, sizeof(encoded_creator_id), "%u", CIRCUITPY_CREATOR_ID);
char encoded_creation_id[11]; // 2 ** 32 is 10 decimal digits plus one for \0
snprintf(encoded_creation_id, sizeof(encoded_creation_id), "%u", CIRCUITPY_CREATION_ID);
const char *hostname = common_hal_mdns_server_get_hostname(&mdns);
_send_chunks(socket,
"{\"web_api_version\": 1, ",
"\"version\": \"", MICROPY_GIT_TAG, "\", ",
"\"build_date\": \"", MICROPY_BUILD_DATE, "\", ",
"\"board_name\": \"", MICROPY_HW_BOARD_NAME, "\", ",
"\"mcu_name\": \"", MICROPY_HW_MCU_NAME, "\", ",
"\"board_id\": \"", CIRCUITPY_BOARD_ID, "\", ",
"\"creator_id\": ", encoded_creator_id, ", ",
"\"creation_id\": ", encoded_creation_id, ", ",
"\"hostname\": \"", hostname, "\", ",
"\"port\": 80, ",
"\"ip\": \"", _our_ip_encoded,
"\"}", NULL);
// Empty chunk signals the end of the response.
_send_chunk(socket, "");
}
@ -806,13 +833,12 @@ static void _reply_static(socketpool_socket_obj_t *socket, _request *request, co
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_strs(socket,
"HTTP/1.1 200 OK\r\n",
"Content-Encoding: gzip\r\n",
"Content-Length: ", encoded_len, "\r\n",
"Content-Type: ", content_type, "\r\n",
"\r\n", NULL);
_send_raw(socket, response, response_len);
}