mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-01 20:29:00 +00:00
examples/webpanel: Add a web panel application to configure NuttX
Initial development of the NuttX's Web Panel application. This is a self-hosted web page application that enables retrieving system info, provides a simple NSH terminal and enables uploading files. Signed-off-by: Tiago Medicci <tiago.medicci@espressif.com>
This commit is contained in:
parent
81a1539d4b
commit
ced7c261cc
16 changed files with 2994 additions and 1 deletions
|
|
@ -8,6 +8,7 @@ exclude-file = .codespell-ignore-lines
|
|||
# Ignore complete files (e.g. legal text or other immutable material).
|
||||
skip =
|
||||
LICENSE,
|
||||
examples/webpanel/content/www/xterm.min.js,
|
||||
|
||||
# Ignore words list (FTP protocol commands and technical terms)
|
||||
ignore-words-list = ALLO, ARCHTYPE, parm
|
||||
ignore-words-list = ALLO, ARCHTYPE, parm, shiftIn
|
||||
|
|
|
|||
2
examples/webpanel/.gitignore
vendored
Normal file
2
examples/webpanel/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# ROMFS build artifacts (generated by Makefile)
|
||||
/content
|
||||
25
examples/webpanel/CMakeLists.txt
Normal file
25
examples/webpanel/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# ##############################################################################
|
||||
# apps/examples/webpanel/CMakeLists.txt
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more contributor
|
||||
# license agreements. See the NOTICE file distributed with this work for
|
||||
# additional information regarding copyright ownership. The ASF licenses this
|
||||
# file to you under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy of
|
||||
# the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
#
|
||||
# ##############################################################################
|
||||
|
||||
if(CONFIG_EXAMPLES_WEBPANEL)
|
||||
nuttx_add_application(NAME ${CONFIG_EXAMPLES_WEBPANEL_PROGNAME})
|
||||
endif()
|
||||
109
examples/webpanel/Kconfig
Normal file
109
examples/webpanel/Kconfig
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#
|
||||
# For a description of the syntax of this configuration file,
|
||||
# see the file kconfig-language.txt in the NuttX tools repository.
|
||||
#
|
||||
|
||||
config EXAMPLES_WEBPANEL
|
||||
tristate "Web Panel for device management"
|
||||
default n
|
||||
depends on NET_TCP
|
||||
depends on NETUTILS_THTTPD
|
||||
depends on NETUTILS_LIBWEBSOCKETS
|
||||
depends on NETUTILS_LIBWEBSOCKETS_SERVER
|
||||
depends on FS_BINFS
|
||||
depends on FS_UNIONFS
|
||||
depends on PSEUDOTERM
|
||||
depends on PSEUDOTERM_SUSV1
|
||||
---help---
|
||||
Enable the Web Panel application, a Tasmota-inspired web
|
||||
interface for NuttX devices. Provides a landing page served
|
||||
via THTTPD from ROMFS, CGI handlers for system info, file
|
||||
management, and a WebSocket-based NSH terminal (powered
|
||||
by libwebsockets).
|
||||
|
||||
if EXAMPLES_WEBPANEL
|
||||
|
||||
config EXAMPLES_WEBPANEL_PROGNAME
|
||||
string "Program name"
|
||||
default "webpanel"
|
||||
|
||||
config EXAMPLES_WEBPANEL_PRIORITY
|
||||
int "Web Panel task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_STACKSIZE
|
||||
int "Web Panel stack size"
|
||||
default 4096
|
||||
|
||||
config EXAMPLES_WEBPANEL_NETIF
|
||||
string "Network interface name"
|
||||
default "eth0"
|
||||
---help---
|
||||
Name of the network interface used by the web panel for
|
||||
system info queries and DHCP renew (e.g. eth0, wlan0).
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_SYSINFO_PROGNAME
|
||||
string "CGI sysinfo program name"
|
||||
default "sysinfo"
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_SYSINFO_PRIORITY
|
||||
int "CGI sysinfo task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_SYSINFO_STACKSIZE
|
||||
int "CGI sysinfo stack size"
|
||||
default 4096
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_FILES_PROGNAME
|
||||
string "CGI files program name"
|
||||
default "files"
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_FILES_PRIORITY
|
||||
int "CGI files task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_FILES_STACKSIZE
|
||||
int "CGI files stack size"
|
||||
default 4096
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_UPLOAD_PROGNAME
|
||||
string "CGI upload program name"
|
||||
default "upload"
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_UPLOAD_PRIORITY
|
||||
int "CGI upload task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_UPLOAD_STACKSIZE
|
||||
int "CGI upload stack size"
|
||||
default 8192
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_RENEW_PROGNAME
|
||||
string "CGI DHCP renew program name"
|
||||
default "dhcprenew"
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_RENEW_PRIORITY
|
||||
int "CGI DHCP renew task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_CGI_RENEW_STACKSIZE
|
||||
int "CGI DHCP renew stack size"
|
||||
default 4096
|
||||
|
||||
config EXAMPLES_WEBPANEL_WS_PORT
|
||||
int "WebSocket terminal TCP port"
|
||||
default 8080
|
||||
|
||||
config EXAMPLES_WEBPANEL_WS_PRIORITY
|
||||
int "WebSocket server task priority"
|
||||
default 100
|
||||
|
||||
config EXAMPLES_WEBPANEL_WS_STACKSIZE
|
||||
int "WebSocket daemon stack size"
|
||||
default 8192
|
||||
|
||||
config EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE
|
||||
int "WebSocket NSH session stack size"
|
||||
default 4096
|
||||
|
||||
endif
|
||||
25
examples/webpanel/Make.defs
Normal file
25
examples/webpanel/Make.defs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
############################################################################
|
||||
# apps/examples/webpanel/Make.defs
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership. The
|
||||
# ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance with the
|
||||
# License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
############################################################################
|
||||
|
||||
ifneq ($(CONFIG_EXAMPLES_WEBPANEL),)
|
||||
CONFIGURED_APPS += $(APPDIR)/examples/webpanel
|
||||
endif
|
||||
94
examples/webpanel/Makefile
Normal file
94
examples/webpanel/Makefile
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
############################################################################
|
||||
# apps/examples/webpanel/Makefile
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership. The
|
||||
# ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance with the
|
||||
# License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
############################################################################
|
||||
|
||||
include $(APPDIR)/Make.defs
|
||||
|
||||
# Web Panel application + CGI handlers
|
||||
# NuttX multi-program build: space-separated lists, matched by position.
|
||||
|
||||
CSRCS = romfs.c ws_terminal.c
|
||||
MAINSRC = webpanel_main.c cgi_sysinfo.c cgi_files.c cgi_upload.c cgi_renew.c
|
||||
|
||||
PROGNAME = $(CONFIG_EXAMPLES_WEBPANEL_PROGNAME) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_PROGNAME) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_PROGNAME) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_PROGNAME) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_PROGNAME)
|
||||
PRIORITY = $(CONFIG_EXAMPLES_WEBPANEL_PRIORITY) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_PRIORITY) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_PRIORITY) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_PRIORITY) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_PRIORITY)
|
||||
STACKSIZE = $(CONFIG_EXAMPLES_WEBPANEL_STACKSIZE) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_SYSINFO_STACKSIZE) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_FILES_STACKSIZE) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_UPLOAD_STACKSIZE) \
|
||||
$(CONFIG_EXAMPLES_WEBPANEL_CGI_RENEW_STACKSIZE)
|
||||
|
||||
LWS_DIR = $(APPDIR)/netutils/libwebsockets
|
||||
CFLAGS += -I$(LWS_DIR) \
|
||||
-I$(LWS_DIR)/libwebsockets/include
|
||||
|
||||
MODULE = $(CONFIG_EXAMPLES_WEBPANEL)
|
||||
|
||||
VPATH += content
|
||||
DEPPATH += --dep-path content
|
||||
|
||||
# ROMFS image generation
|
||||
# Static web content goes directly into the ROMFS root (no www/ subdir)
|
||||
|
||||
WEBPANEL_DIR = $(APPDIR)/examples/webpanel
|
||||
CONTENT_DIR = $(WEBPANEL_DIR)/content
|
||||
ROMFS_DIR = $(CONTENT_DIR)/romfs
|
||||
ROMFS_IMG = $(CONTENT_DIR)/romfs.img
|
||||
ROMFS_SRC = $(CONTENT_DIR)/romfs.c
|
||||
|
||||
WWW_FILES = $(wildcard $(CONTENT_DIR)/www/*)
|
||||
|
||||
$(CONTENT_DIR)/.romfs_stamp: $(WWW_FILES)
|
||||
$(Q) mkdir -p $(ROMFS_DIR)
|
||||
$(Q) cp -a $(CONTENT_DIR)/www/* $(ROMFS_DIR)/
|
||||
$(Q) touch $@
|
||||
|
||||
$(ROMFS_IMG): $(CONTENT_DIR)/.romfs_stamp
|
||||
$(Q) genromfs -f $@ -d $(ROMFS_DIR) -V "WebPanelROMFS"
|
||||
|
||||
$(ROMFS_SRC): $(ROMFS_IMG)
|
||||
$(Q) (cd $(CONTENT_DIR) && \
|
||||
echo "#include <nuttx/compiler.h>" >$@ && \
|
||||
xxd -i romfs.img | \
|
||||
sed -e "s/romfs_img/webpanel_romfs_img/g" | \
|
||||
sed -e "s/^unsigned char/const unsigned char aligned_data(4)/g" >>$@)
|
||||
|
||||
content/romfs.c: $(ROMFS_SRC)
|
||||
|
||||
context::
|
||||
|
||||
clean::
|
||||
$(call DELFILE, $(ROMFS_SRC))
|
||||
$(call DELFILE, $(ROMFS_IMG))
|
||||
$(call DELFILE, $(CONTENT_DIR)/.romfs_stamp)
|
||||
$(Q) rm -rf $(ROMFS_DIR)
|
||||
|
||||
distclean:: clean
|
||||
|
||||
include $(APPDIR)/Application.mk
|
||||
314
examples/webpanel/cgi_files.c
Normal file
314
examples/webpanel/cgi_files.c
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/cgi_files.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/****************************************************************************
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
#define FILES_DIR "/mnt"
|
||||
|
||||
/****************************************************************************
|
||||
* Private Function Prototypes
|
||||
****************************************************************************/
|
||||
|
||||
static void url_decode(char *dst, const char *src, size_t dstsize);
|
||||
static const char *get_query_param(const char *qs, const char *key,
|
||||
char *val, size_t vallen);
|
||||
static void handle_list(void);
|
||||
static void handle_delete(const char *name);
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: url_decode
|
||||
*
|
||||
* Description:
|
||||
* Decode a URL-encoded string into a destination buffer.
|
||||
*
|
||||
* Input Parameters:
|
||||
* dst - Destination buffer.
|
||||
* src - Source URL-encoded string.
|
||||
* dstsize - Size of destination buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void url_decode(char *dst, const char *src, size_t dstsize)
|
||||
{
|
||||
size_t i = 0;
|
||||
|
||||
while (*src && i < dstsize - 1)
|
||||
{
|
||||
if (*src == '%' && src[1] && src[2])
|
||||
{
|
||||
char hex[3];
|
||||
|
||||
hex[0] = src[1];
|
||||
hex[1] = src[2];
|
||||
hex[2] = '\0';
|
||||
dst[i++] = (char)strtol(hex, NULL, 16);
|
||||
src += 3;
|
||||
}
|
||||
else if (*src == '+')
|
||||
{
|
||||
dst[i++] = ' ';
|
||||
src++;
|
||||
}
|
||||
else
|
||||
{
|
||||
dst[i++] = *src++;
|
||||
}
|
||||
}
|
||||
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: get_query_param
|
||||
*
|
||||
* Description:
|
||||
* Find and decode a key=value parameter from the QUERY_STRING.
|
||||
*
|
||||
* Input Parameters:
|
||||
* qs - Query string.
|
||||
* key - Parameter name to search for.
|
||||
* val - Output buffer for decoded value.
|
||||
* vallen - Size of output buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* Pointer to val on success; NULL if not found or invalid input.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static const char *get_query_param(const char *qs, const char *key,
|
||||
char *val, size_t vallen)
|
||||
{
|
||||
const char *p;
|
||||
size_t klen;
|
||||
|
||||
if (qs == NULL || key == NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
klen = strlen(key);
|
||||
p = qs;
|
||||
|
||||
while (*p)
|
||||
{
|
||||
if (strncmp(p, key, klen) == 0 && p[klen] == '=')
|
||||
{
|
||||
const char *start = p + klen + 1;
|
||||
const char *end = strchr(start, '&');
|
||||
size_t len;
|
||||
|
||||
if (end == NULL)
|
||||
{
|
||||
end = start + strlen(start);
|
||||
}
|
||||
|
||||
len = end - start;
|
||||
if (len >= vallen)
|
||||
{
|
||||
len = vallen - 1;
|
||||
}
|
||||
|
||||
url_decode(val, start, len + 1);
|
||||
return val;
|
||||
}
|
||||
|
||||
p = strchr(p, '&');
|
||||
if (p == NULL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
p++;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: handle_list
|
||||
*
|
||||
* Description:
|
||||
* Emit a JSON response listing files in FILES_DIR.
|
||||
*
|
||||
* Input Parameters:
|
||||
* None.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void handle_list(void)
|
||||
{
|
||||
DIR *dir;
|
||||
struct dirent *ent;
|
||||
struct stat st;
|
||||
char path[128];
|
||||
int first = 1;
|
||||
|
||||
puts("Content-type: application/json\r\n"
|
||||
"\r\n");
|
||||
|
||||
printf("{\"files\":[");
|
||||
|
||||
dir = opendir(FILES_DIR);
|
||||
if (dir != NULL)
|
||||
{
|
||||
while ((ent = readdir(dir)) != NULL)
|
||||
{
|
||||
if (ent->d_name[0] == '.')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s", FILES_DIR, ent->d_name);
|
||||
|
||||
if (!first)
|
||||
{
|
||||
printf(",");
|
||||
}
|
||||
|
||||
first = 0;
|
||||
|
||||
if (stat(path, &st) == 0)
|
||||
{
|
||||
printf("{\"name\":\"%s\",\"size\":%ld}",
|
||||
ent->d_name, (long)st.st_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("{\"name\":\"%s\",\"size\":0}", ent->d_name);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
printf("]}\n");
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: handle_delete
|
||||
*
|
||||
* Description:
|
||||
* Delete the requested file from FILES_DIR and emit a JSON response.
|
||||
*
|
||||
* Input Parameters:
|
||||
* name - File name to delete.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void handle_delete(const char *name)
|
||||
{
|
||||
char path[128];
|
||||
|
||||
if (name == NULL || name[0] == '\0' || strchr(name, '/') != NULL)
|
||||
{
|
||||
puts("Content-type: application/json\r\n"
|
||||
"Status: 400\r\n"
|
||||
"\r\n");
|
||||
printf("{\"error\":\"Invalid filename\"}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(path, sizeof(path), "%s/%s", FILES_DIR, name);
|
||||
|
||||
if (unlink(path) == 0)
|
||||
{
|
||||
puts("Content-type: application/json\r\n"
|
||||
"\r\n");
|
||||
printf("{\"ok\":true}\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
puts("Content-type: application/json\r\n"
|
||||
"Status: 404\r\n"
|
||||
"\r\n");
|
||||
printf("{\"error\":\"File not found\"}\n");
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: files_main
|
||||
*
|
||||
* Description:
|
||||
* Entry point for file list/delete CGI handling.
|
||||
*
|
||||
* Input Parameters:
|
||||
* argc - Number of arguments.
|
||||
* argv - Argument vector.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero (OK).
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int files_main(int argc, FAR char *argv[])
|
||||
{
|
||||
const char *qs;
|
||||
char action[16];
|
||||
char name[64];
|
||||
|
||||
qs = getenv("QUERY_STRING");
|
||||
|
||||
if (qs != NULL && get_query_param(qs, "action", action, sizeof(action)))
|
||||
{
|
||||
if (strcmp(action, "delete") == 0)
|
||||
{
|
||||
get_query_param(qs, "name", name, sizeof(name));
|
||||
handle_delete(name);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
handle_list();
|
||||
return 0;
|
||||
}
|
||||
72
examples/webpanel/cgi_renew.c
Normal file
72
examples/webpanel/cgi_renew.c
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/cgi_renew.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "netutils/netlib.h"
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: dhcprenew_main
|
||||
*
|
||||
* Description:
|
||||
* CGI entry point that renews the IPv4 address on the configured
|
||||
* interface and returns the result as JSON.
|
||||
*
|
||||
* Input Parameters:
|
||||
* argc - Number of arguments.
|
||||
* argv - Argument vector.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero on success; one on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int dhcprenew_main(int argc, FAR char *argv[])
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = netlib_obtain_ipv4addr(CONFIG_EXAMPLES_WEBPANEL_NETIF);
|
||||
|
||||
puts("Content-type: application/json\r\n"
|
||||
"\r\n");
|
||||
|
||||
if (ret >= 0)
|
||||
{
|
||||
puts("{\"status\":\"ok\"}\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("{\"status\":\"error\",\"code\":%d}\n", ret);
|
||||
}
|
||||
|
||||
return ret < 0 ? 1 : 0;
|
||||
}
|
||||
414
examples/webpanel/cgi_sysinfo.c
Normal file
414
examples/webpanel/cgi_sysinfo.c
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/cgi_sysinfo.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <net/if.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
/****************************************************************************
|
||||
* Private Function Prototypes
|
||||
****************************************************************************/
|
||||
|
||||
static void get_version(char *ver, size_t vlen,
|
||||
char *build, size_t blen,
|
||||
char *board, size_t boardlen);
|
||||
static void get_arch(char *arch, size_t archlen);
|
||||
static unsigned long get_uptime(char *buf, size_t len);
|
||||
static void get_net_info(char *ip, size_t iplen,
|
||||
char *mask, size_t masklen,
|
||||
char *gw, size_t gwlen,
|
||||
char *mac, size_t maclen);
|
||||
static int count_files(const char *path);
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: get_version
|
||||
*
|
||||
* Description:
|
||||
* Read version, build hash, and board information from /proc/version.
|
||||
*
|
||||
* Input Parameters:
|
||||
* ver - Output buffer for version string.
|
||||
* vlen - Size of ver buffer.
|
||||
* build - Output buffer for build/hash string.
|
||||
* blen - Size of build buffer.
|
||||
* board - Output buffer for board string.
|
||||
* boardlen - Size of board buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void get_version(char *ver, size_t vlen,
|
||||
char *build, size_t blen,
|
||||
char *board, size_t boardlen)
|
||||
{
|
||||
FILE *fp;
|
||||
char line[256];
|
||||
char *p;
|
||||
char *sp;
|
||||
char *last;
|
||||
char *tok;
|
||||
size_t n;
|
||||
|
||||
strncpy(ver, "unknown", vlen);
|
||||
strncpy(build, "unknown", blen);
|
||||
strncpy(board, "unknown", boardlen);
|
||||
|
||||
fp = fopen("/proc/version", "r");
|
||||
if (fp == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fgets(line, sizeof(line), fp) != NULL)
|
||||
{
|
||||
/* Format: "NuttX version X.Y.Z HASH DATE TIME BOARD:CONFIG" */
|
||||
|
||||
p = strstr(line, "version ");
|
||||
if (p != NULL)
|
||||
{
|
||||
p += 8;
|
||||
|
||||
/* version token */
|
||||
|
||||
sp = strchr(p, ' ');
|
||||
if (sp != NULL)
|
||||
{
|
||||
n = sp - p;
|
||||
if (n >= vlen)
|
||||
{
|
||||
n = vlen - 1;
|
||||
}
|
||||
|
||||
memcpy(ver, p, n);
|
||||
ver[n] = '\0';
|
||||
|
||||
/* build (git hash) token */
|
||||
|
||||
p = sp + 1;
|
||||
sp = strchr(p, ' ');
|
||||
if (sp != NULL)
|
||||
{
|
||||
n = sp - p;
|
||||
if (n >= blen)
|
||||
{
|
||||
n = blen - 1;
|
||||
}
|
||||
|
||||
memcpy(build, p, n);
|
||||
build[n] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Board name is the last whitespace-separated token; trim :config */
|
||||
|
||||
last = NULL;
|
||||
tok = strtok(line, " \t\r\n");
|
||||
while (tok != NULL)
|
||||
{
|
||||
last = tok;
|
||||
tok = strtok(NULL, " \t\r\n");
|
||||
}
|
||||
|
||||
if (last != NULL)
|
||||
{
|
||||
char *colon = strchr(last, ':');
|
||||
if (colon != NULL)
|
||||
{
|
||||
*colon = '\0';
|
||||
}
|
||||
|
||||
strncpy(board, last, boardlen - 1);
|
||||
board[boardlen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: get_arch
|
||||
*
|
||||
* Description:
|
||||
* Query architecture information via uname().
|
||||
*
|
||||
* Input Parameters:
|
||||
* arch - Output buffer for architecture string.
|
||||
* archlen - Size of arch buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void get_arch(char *arch, size_t archlen)
|
||||
{
|
||||
struct utsname uts;
|
||||
|
||||
strncpy(arch, "unknown", archlen);
|
||||
|
||||
if (uname(&uts) == 0)
|
||||
{
|
||||
strncpy(arch, uts.machine, archlen - 1);
|
||||
arch[archlen - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: get_uptime
|
||||
*
|
||||
* Description:
|
||||
* Read system uptime based on CLOCK_MONOTONIC and format it.
|
||||
*
|
||||
* Input Parameters:
|
||||
* buf - Output buffer for formatted uptime.
|
||||
* len - Size of buf.
|
||||
*
|
||||
* Returned Value:
|
||||
* Uptime in seconds; zero on error.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static unsigned long get_uptime(char *buf, size_t len)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
|
||||
{
|
||||
unsigned long secs = ts.tv_sec;
|
||||
unsigned int days = secs / 86400;
|
||||
unsigned int hrs = (secs % 86400) / 3600;
|
||||
unsigned int mins = (secs % 3600) / 60;
|
||||
unsigned int s = secs % 60;
|
||||
|
||||
if (days > 0)
|
||||
{
|
||||
snprintf(buf, len, "%ud %02u:%02u:%02u", days, hrs, mins, s);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(buf, len, "%02u:%02u:%02u", hrs, mins, s);
|
||||
}
|
||||
|
||||
return secs;
|
||||
}
|
||||
|
||||
snprintf(buf, len, "unknown");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: get_net_info
|
||||
*
|
||||
* Description:
|
||||
* Query IP, netmask, gateway, and MAC for the configured interface.
|
||||
*
|
||||
* Input Parameters:
|
||||
* ip - Output buffer for IPv4 address.
|
||||
* iplen - Size of ip buffer.
|
||||
* mask - Output buffer for netmask.
|
||||
* masklen - Size of mask buffer.
|
||||
* gw - Output buffer for gateway.
|
||||
* gwlen - Size of gw buffer.
|
||||
* mac - Output buffer for MAC address.
|
||||
* maclen - Size of mac buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void get_net_info(char *ip, size_t iplen,
|
||||
char *mask, size_t masklen,
|
||||
char *gw, size_t gwlen,
|
||||
char *mac, size_t maclen)
|
||||
{
|
||||
int sockfd;
|
||||
struct ifreq ifr;
|
||||
|
||||
strncpy(ip, "N/A", iplen);
|
||||
strncpy(mask, "N/A", masklen);
|
||||
strncpy(gw, "N/A", gwlen);
|
||||
strncpy(mac, "N/A", maclen);
|
||||
|
||||
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sockfd < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
strncpy(ifr.ifr_name, CONFIG_EXAMPLES_WEBPANEL_NETIF, IFNAMSIZ);
|
||||
|
||||
if (ioctl(sockfd, SIOCGIFADDR, &ifr) == 0)
|
||||
{
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
|
||||
inet_ntop(AF_INET, &sa->sin_addr, ip, iplen);
|
||||
}
|
||||
|
||||
if (ioctl(sockfd, SIOCGIFNETMASK, &ifr) == 0)
|
||||
{
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_netmask;
|
||||
inet_ntop(AF_INET, &sa->sin_addr, mask, masklen);
|
||||
}
|
||||
|
||||
if (ioctl(sockfd, SIOCGIFDSTADDR, &ifr) == 0)
|
||||
{
|
||||
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_dstaddr;
|
||||
inet_ntop(AF_INET, &sa->sin_addr, gw, gwlen);
|
||||
}
|
||||
|
||||
if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) == 0)
|
||||
{
|
||||
unsigned char *hw = (unsigned char *)ifr.ifr_hwaddr.sa_data;
|
||||
snprintf(mac, maclen, "%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
hw[0], hw[1], hw[2], hw[3], hw[4], hw[5]);
|
||||
}
|
||||
|
||||
close(sockfd);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: count_files
|
||||
*
|
||||
* Description:
|
||||
* Count visible files in a directory.
|
||||
*
|
||||
* Input Parameters:
|
||||
* path - Directory path.
|
||||
*
|
||||
* Returned Value:
|
||||
* Number of visible entries.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int count_files(const char *path)
|
||||
{
|
||||
DIR *dir;
|
||||
struct dirent *ent;
|
||||
int count = 0;
|
||||
|
||||
dir = opendir(path);
|
||||
if (dir == NULL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
while ((ent = readdir(dir)) != NULL)
|
||||
{
|
||||
if (ent->d_name[0] != '.')
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: sysinfo_main
|
||||
*
|
||||
* Description:
|
||||
* CGI entry point that returns board and runtime information as JSON.
|
||||
*
|
||||
* Input Parameters:
|
||||
* argc - Number of arguments.
|
||||
* argv - Argument vector.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero (OK).
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int sysinfo_main(int argc, FAR char *argv[])
|
||||
{
|
||||
char version[16];
|
||||
char build[16];
|
||||
char board[64];
|
||||
char arch[32];
|
||||
char uptime[32];
|
||||
char ip[16];
|
||||
char mask[16];
|
||||
char gw[16];
|
||||
char mac[24];
|
||||
unsigned long uptime_sec;
|
||||
int nfiles;
|
||||
|
||||
get_version(version, sizeof(version), build, sizeof(build),
|
||||
board, sizeof(board));
|
||||
get_arch(arch, sizeof(arch));
|
||||
uptime_sec = get_uptime(uptime, sizeof(uptime));
|
||||
get_net_info(ip, sizeof(ip), mask, sizeof(mask),
|
||||
gw, sizeof(gw), mac, sizeof(mac));
|
||||
nfiles = count_files("/mnt");
|
||||
|
||||
puts("Content-type: application/json\r\n"
|
||||
"\r\n");
|
||||
|
||||
printf("{"
|
||||
"\"chip\":\"" CONFIG_ARCH_CHIP "\","
|
||||
"\"board\":\"%s\","
|
||||
"\"os\":\"NuttX\","
|
||||
"\"version\":\"%s\","
|
||||
"\"build\":\"%s\","
|
||||
"\"arch\":\"%s\","
|
||||
"\"ifname\":\"" CONFIG_EXAMPLES_WEBPANEL_NETIF "\","
|
||||
"\"ip\":\"%s\","
|
||||
"\"mask\":\"%s\","
|
||||
"\"gw\":\"%s\","
|
||||
"\"mac\":\"%s\","
|
||||
"\"uptime\":\"%s\","
|
||||
"\"uptime_sec\":%lu,"
|
||||
"\"files\":%d"
|
||||
"}\n",
|
||||
board, version, build, arch,
|
||||
ip, mask, gw, mac,
|
||||
uptime, uptime_sec, nfiles);
|
||||
|
||||
return 0;
|
||||
}
|
||||
637
examples/webpanel/cgi_upload.c
Normal file
637
examples/webpanel/cgi_upload.c
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/cgi_upload.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
|
||||
/****************************************************************************
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
#define UPLOAD_DIR "/mnt"
|
||||
#define BUF_SIZE 512
|
||||
#define MAX_FILENAME 64
|
||||
#define MAX_BOUNDARY 80
|
||||
|
||||
/****************************************************************************
|
||||
* Private Function Prototypes
|
||||
****************************************************************************/
|
||||
|
||||
static void cgi_error(int status, const char *msg);
|
||||
static void cgi_ok(const char *filename);
|
||||
static int extract_boundary(const char *content_type, char *boundary,
|
||||
size_t blen);
|
||||
static int extract_filename(const char *line, char *name, size_t nlen);
|
||||
static size_t read_stdin(char *buf, size_t n);
|
||||
static char *memmem_local(const char *haystack, size_t hlen,
|
||||
const char *needle, size_t nlen);
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: cgi_error
|
||||
*
|
||||
* Description:
|
||||
* Emit a JSON error response with HTTP status.
|
||||
*
|
||||
* Input Parameters:
|
||||
* status - HTTP status code.
|
||||
* msg - Error message string.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void cgi_error(int status, const char *msg)
|
||||
{
|
||||
printf("Content-type: application/json\r\n"
|
||||
"Status: %d\r\n"
|
||||
"\r\n"
|
||||
"{\"error\":\"%s\"}\n", status, msg);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: cgi_ok
|
||||
*
|
||||
* Description:
|
||||
* Emit a JSON success response.
|
||||
*
|
||||
* Input Parameters:
|
||||
* filename - Uploaded file name.
|
||||
*
|
||||
* Returned Value:
|
||||
* None.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void cgi_ok(const char *filename)
|
||||
{
|
||||
printf("Content-type: application/json\r\n"
|
||||
"\r\n"
|
||||
"{\"ok\":true,\"name\":\"%s\"}\n", filename);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: extract_boundary
|
||||
*
|
||||
* Description:
|
||||
* Extract multipart boundary from CONTENT_TYPE.
|
||||
*
|
||||
* Input Parameters:
|
||||
* content_type - CONTENT_TYPE value.
|
||||
* boundary - Output buffer for boundary token.
|
||||
* blen - Size of boundary buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero on success; negated value on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int extract_boundary(const char *content_type, char *boundary,
|
||||
size_t blen)
|
||||
{
|
||||
const char *p;
|
||||
char *q;
|
||||
size_t len;
|
||||
|
||||
p = strstr(content_type, "boundary=");
|
||||
if (p == NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
p += 9;
|
||||
|
||||
/* Skip optional quotes */
|
||||
|
||||
if (*p == '"')
|
||||
{
|
||||
p++;
|
||||
}
|
||||
|
||||
strncpy(boundary, p, blen - 1);
|
||||
boundary[blen - 1] = '\0';
|
||||
|
||||
/* Remove trailing quote if present */
|
||||
|
||||
q = strchr(boundary, '"');
|
||||
if (q != NULL)
|
||||
{
|
||||
*q = '\0';
|
||||
}
|
||||
|
||||
/* Remove trailing whitespace/CR/LF */
|
||||
|
||||
len = strlen(boundary);
|
||||
while (len > 0 &&
|
||||
(boundary[len - 1] == '\r' || boundary[len - 1] == '\n' ||
|
||||
boundary[len - 1] == ' '))
|
||||
{
|
||||
boundary[--len] = '\0';
|
||||
}
|
||||
|
||||
return len > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: extract_filename
|
||||
*
|
||||
* Description:
|
||||
* Extract a safe filename from a Content-Disposition header line.
|
||||
*
|
||||
* Input Parameters:
|
||||
* line - Header line.
|
||||
* name - Output buffer for filename.
|
||||
* nlen - Size of name buffer.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero on success; negated value on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int extract_filename(const char *line, char *name, size_t nlen)
|
||||
{
|
||||
const char *p;
|
||||
const char *end;
|
||||
size_t len;
|
||||
|
||||
p = strstr(line, "filename=\"");
|
||||
if (p == NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
p += 10;
|
||||
end = strchr(p, '"');
|
||||
if (end == NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = end - p;
|
||||
if (len == 0 || len >= nlen)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Reject path separators in filename */
|
||||
|
||||
if (memchr(p, '/', len) != NULL || memchr(p, '\\', len) != NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(name, p, len);
|
||||
name[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: read_stdin
|
||||
*
|
||||
* Description:
|
||||
* Read exactly n bytes from standard input unless EOF/error occurs.
|
||||
*
|
||||
* Input Parameters:
|
||||
* buf - Destination buffer.
|
||||
* n - Requested number of bytes.
|
||||
*
|
||||
* Returned Value:
|
||||
* Number of bytes actually read.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static size_t read_stdin(char *buf, size_t n)
|
||||
{
|
||||
size_t total = 0;
|
||||
while (total < n)
|
||||
{
|
||||
ssize_t r = read(STDIN_FILENO, buf + total, n - total);
|
||||
if (r <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
total += r;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: memmem_local
|
||||
*
|
||||
* Description:
|
||||
* Find a byte sequence inside a bounded memory region.
|
||||
*
|
||||
* Input Parameters:
|
||||
* haystack - Input buffer.
|
||||
* hlen - Size of haystack.
|
||||
* needle - Pattern to search.
|
||||
* nlen - Size of needle.
|
||||
*
|
||||
* Returned Value:
|
||||
* Pointer to first match; NULL if not found.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static char *memmem_local(const char *haystack, size_t hlen,
|
||||
const char *needle, size_t nlen)
|
||||
{
|
||||
const char *p;
|
||||
if (nlen == 0)
|
||||
{
|
||||
return (char *)haystack;
|
||||
}
|
||||
|
||||
if (hlen < nlen)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
p = haystack;
|
||||
while (p <= haystack + hlen - nlen)
|
||||
{
|
||||
if (memcmp(p, needle, nlen) == 0)
|
||||
{
|
||||
return (char *)p;
|
||||
}
|
||||
|
||||
p++;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: upload_main
|
||||
*
|
||||
* Description:
|
||||
* CGI entry point for multipart file upload.
|
||||
*
|
||||
* Input Parameters:
|
||||
* argc - Number of arguments.
|
||||
* argv - Argument vector.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero (OK).
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int upload_main(int argc, FAR char *argv[])
|
||||
{
|
||||
const char *content_type;
|
||||
const char *content_length_str;
|
||||
char boundary_raw[MAX_BOUNDARY];
|
||||
char boundary[MAX_BOUNDARY + 4];
|
||||
size_t boundary_len;
|
||||
char filename[MAX_FILENAME];
|
||||
char filepath[MAX_FILENAME + 8];
|
||||
char buf[BUF_SIZE];
|
||||
size_t content_length;
|
||||
size_t total_read;
|
||||
int fd = -1;
|
||||
int state;
|
||||
char linebuf[256];
|
||||
size_t linepos;
|
||||
int headers_done;
|
||||
|
||||
content_type = getenv("CONTENT_TYPE");
|
||||
content_length_str = getenv("CONTENT_LENGTH");
|
||||
|
||||
if (content_type == NULL || content_length_str == NULL)
|
||||
{
|
||||
cgi_error(400, "Missing Content-Type or Content-Length");
|
||||
return 0;
|
||||
}
|
||||
|
||||
content_length = strtoul(content_length_str, NULL, 10);
|
||||
if (content_length == 0 || content_length > 1024 * 1024)
|
||||
{
|
||||
cgi_error(400, "Invalid content length (max 1MB)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (extract_boundary(content_type, boundary_raw, sizeof(boundary_raw)) < 0)
|
||||
{
|
||||
cgi_error(400, "No boundary in Content-Type");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Multipart boundaries in the body are prefixed with "--" */
|
||||
|
||||
snprintf(boundary, sizeof(boundary), "--%s", boundary_raw);
|
||||
boundary_len = strlen(boundary);
|
||||
|
||||
/* State machine for multipart parsing:
|
||||
* 0 = looking for first boundary
|
||||
* 1 = reading headers after boundary
|
||||
* 2 = reading file data
|
||||
* 3 = done
|
||||
*/
|
||||
|
||||
state = 0;
|
||||
total_read = 0;
|
||||
filename[0] = '\0';
|
||||
linepos = 0;
|
||||
headers_done = 0;
|
||||
|
||||
/* Read the entire POST body in chunks and process inline.
|
||||
* We accumulate a line buffer for header parsing, and stream
|
||||
* file data directly to disk.
|
||||
*/
|
||||
|
||||
while (total_read < content_length && state != 3)
|
||||
{
|
||||
size_t toread = content_length - total_read;
|
||||
size_t nread;
|
||||
|
||||
if (toread > sizeof(buf))
|
||||
{
|
||||
toread = sizeof(buf);
|
||||
}
|
||||
|
||||
nread = read_stdin(buf, toread);
|
||||
if (nread == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
total_read += nread;
|
||||
|
||||
size_t i = 0;
|
||||
while (i < nread && state != 3)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
|
||||
/* Accumulate until we find the first boundary line */
|
||||
|
||||
while (i < nread)
|
||||
{
|
||||
if (buf[i] == '\n')
|
||||
{
|
||||
linebuf[linepos] = '\0';
|
||||
|
||||
/* Strip trailing \r */
|
||||
|
||||
if (linepos > 0 && linebuf[linepos - 1] == '\r')
|
||||
{
|
||||
linebuf[--linepos] = '\0';
|
||||
}
|
||||
|
||||
if (strncmp(linebuf, boundary, boundary_len) == 0)
|
||||
{
|
||||
state = 1;
|
||||
headers_done = 0;
|
||||
linepos = 0;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
|
||||
linepos = 0;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (linepos < sizeof(linebuf) - 1)
|
||||
{
|
||||
linebuf[linepos++] = buf[i];
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 1:
|
||||
/* Parse headers after boundary, looking for
|
||||
* Content-Disposition with filename.
|
||||
* Headers end with an empty line.
|
||||
*/
|
||||
|
||||
while (i < nread && !headers_done)
|
||||
{
|
||||
if (buf[i] == '\n')
|
||||
{
|
||||
linebuf[linepos] = '\0';
|
||||
|
||||
if (linepos > 0 && linebuf[linepos - 1] == '\r')
|
||||
{
|
||||
linebuf[--linepos] = '\0';
|
||||
}
|
||||
|
||||
if (linepos == 0)
|
||||
{
|
||||
/* Empty line = end of headers */
|
||||
|
||||
headers_done = 1;
|
||||
|
||||
if (filename[0] == '\0')
|
||||
{
|
||||
cgi_error(400, "No filename in upload");
|
||||
if (fd >= 0)
|
||||
{
|
||||
close(fd);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
snprintf(filepath, sizeof(filepath), "%s/%s",
|
||||
UPLOAD_DIR, filename);
|
||||
|
||||
fd = open(filepath,
|
||||
O_WRONLY | O_CREAT | O_TRUNC,
|
||||
0666);
|
||||
if (fd < 0)
|
||||
{
|
||||
cgi_error(500, "Cannot create file");
|
||||
return 0;
|
||||
}
|
||||
|
||||
state = 2;
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (strstr(linebuf, "Content-Disposition") != NULL)
|
||||
{
|
||||
extract_filename(linebuf, filename,
|
||||
sizeof(filename));
|
||||
}
|
||||
|
||||
linepos = 0;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (linepos < sizeof(linebuf) - 1)
|
||||
{
|
||||
linebuf[linepos++] = buf[i];
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
/* Write file data. We need to detect the closing boundary
|
||||
* which appears as "\r\n--BOUNDARY" in the stream.
|
||||
* Buffer the last boundary_len+4 bytes to check for the
|
||||
* boundary.
|
||||
*/
|
||||
|
||||
size_t remaining = nread - i;
|
||||
char *bnd;
|
||||
|
||||
bnd = memmem_local(buf + i, remaining,
|
||||
boundary, boundary_len);
|
||||
if (bnd != NULL)
|
||||
{
|
||||
/* Found boundary. Write everything before it,
|
||||
* minus the preceding \r\n.
|
||||
*/
|
||||
|
||||
size_t datalen = bnd - (buf + i);
|
||||
if (datalen >= 2)
|
||||
{
|
||||
datalen -= 2;
|
||||
}
|
||||
|
||||
if (datalen > 0)
|
||||
{
|
||||
write(fd, buf + i, datalen);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
fd = -1;
|
||||
state = 3;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* No boundary in this chunk. Write data but hold back
|
||||
* enough bytes to avoid splitting a boundary across
|
||||
* chunks.
|
||||
*/
|
||||
|
||||
size_t safe;
|
||||
|
||||
if (remaining > boundary_len + 4)
|
||||
{
|
||||
safe = remaining - boundary_len - 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
safe = 0;
|
||||
}
|
||||
|
||||
if (safe > 0)
|
||||
{
|
||||
write(fd, buf + i, safe);
|
||||
i += safe;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Not enough data to be safe. This means we're near
|
||||
* the end of a chunk and need more data. Just write
|
||||
* what we have and hope the boundary comes in the
|
||||
* next read. For simplicity, write it all - worst
|
||||
* case we include a few extra bytes at end of file
|
||||
* which will be fixed when the boundary is found.
|
||||
*/
|
||||
|
||||
write(fd, buf + i, remaining);
|
||||
i = nread;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Drain any remaining data from stdin */
|
||||
|
||||
while (total_read < content_length)
|
||||
{
|
||||
size_t toread = content_length - total_read;
|
||||
size_t nread;
|
||||
|
||||
if (toread > sizeof(buf))
|
||||
{
|
||||
toread = sizeof(buf);
|
||||
}
|
||||
|
||||
nread = read_stdin(buf, toread);
|
||||
if (nread == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
total_read += nread;
|
||||
}
|
||||
|
||||
if (fd >= 0)
|
||||
{
|
||||
close(fd);
|
||||
}
|
||||
|
||||
if (state == 3 && filename[0] != '\0')
|
||||
{
|
||||
cgi_ok(filename);
|
||||
}
|
||||
else if (filename[0] != '\0')
|
||||
{
|
||||
cgi_ok(filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
cgi_error(400, "Upload incomplete or no file received");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
454
examples/webpanel/content/www/index.html
Normal file
454
examples/webpanel/content/www/index.html
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>NuttX Web Panel</title>
|
||||
<link rel="stylesheet" href="/xterm.css">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#1a1a2e;color:#e0e0e0;min-height:100vh}
|
||||
a{color:#5dade2;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
|
||||
.hdr{background:#16213e;padding:12px 20px;border-bottom:2px solid #0f3460;
|
||||
display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap}
|
||||
.hdr h1{font-size:18px;color:#e94560;display:flex;align-items:center;gap:8px}
|
||||
.hdr .chip{font-size:11px;background:#0f3460;color:#7ec8e3;padding:2px 8px;
|
||||
border-radius:10px}
|
||||
.hdr .uptime{font-size:11px;color:#8899aa}
|
||||
|
||||
nav{background:#0f1b33;display:flex;gap:0;overflow-x:auto}
|
||||
nav a{padding:10px 18px;font-size:13px;color:#8899aa;border-bottom:2px solid transparent;
|
||||
white-space:nowrap;transition:all .2s}
|
||||
nav a:hover,nav a.act{color:#e94560;border-bottom-color:#e94560;text-decoration:none;
|
||||
background:rgba(233,69,96,.06)}
|
||||
|
||||
.page{display:none;padding:16px 20px;max-width:1200px;margin:0 auto}
|
||||
.page.vis{display:block}
|
||||
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
|
||||
.card{background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:16px;
|
||||
transition:border-color .2s}
|
||||
.card:hover{border-color:#e94560}
|
||||
.card h3{font-size:14px;color:#e94560;margin-bottom:10px;display:flex;
|
||||
align-items:center;gap:6px}
|
||||
.card .ico{font-size:18px}
|
||||
.row{display:flex;justify-content:space-between;padding:4px 0;font-size:12px;
|
||||
border-bottom:1px solid rgba(255,255,255,.04)}
|
||||
.row .lbl{color:#8899aa}
|
||||
.row .val{color:#c0d0e0;font-family:monospace}
|
||||
|
||||
.badge{display:inline-block;padding:1px 7px;border-radius:8px;font-size:10px;
|
||||
font-weight:600}
|
||||
.badge-ok{background:#1e4d2b;color:#4ade80}
|
||||
.badge-warn{background:#4d3e1e;color:#fbbf24}
|
||||
|
||||
.term-wrap{background:#0d1117;border:1px solid #0f3460;border-radius:8px;
|
||||
padding:8px;height:360px;overflow:hidden;position:relative}
|
||||
.term-placeholder{display:flex;align-items:center;justify-content:center;
|
||||
height:100%;color:#556;font-size:13px;flex-direction:column;gap:8px}
|
||||
|
||||
.fm-toolbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center}
|
||||
.fm-path{background:#0d1117;border:1px solid #0f3460;border-radius:4px;padding:6px 10px;
|
||||
color:#c0d0e0;font-family:monospace;font-size:12px;flex:1;min-width:120px}
|
||||
.btn{background:#e94560;color:#fff;border:none;padding:7px 16px;border-radius:4px;
|
||||
cursor:pointer;font-size:12px;font-weight:600;transition:background .2s}
|
||||
.btn:hover{background:#c73852}
|
||||
.btn-sec{background:#0f3460;color:#7ec8e3}
|
||||
.btn-sec:hover{background:#1a4a80}
|
||||
|
||||
.fm-list{background:#0d1117;border:1px solid #0f3460;border-radius:8px;overflow:hidden}
|
||||
.fm-item{display:flex;align-items:center;padding:8px 12px;font-size:12px;
|
||||
border-bottom:1px solid rgba(255,255,255,.04);gap:10px}
|
||||
.fm-item:hover{background:rgba(233,69,96,.04)}
|
||||
.fm-item .name{flex:1;font-family:monospace;color:#c0d0e0}
|
||||
.fm-item .sz{color:#667;min-width:60px;text-align:right}
|
||||
.fm-item .acts{display:flex;gap:6px}
|
||||
.fm-item .acts button{background:none;border:1px solid #0f3460;color:#8899aa;
|
||||
padding:2px 8px;border-radius:3px;cursor:pointer;font-size:11px}
|
||||
.fm-item .acts button:hover{border-color:#e94560;color:#e94560}
|
||||
.fm-empty{padding:24px;text-align:center;color:#556;font-size:13px}
|
||||
|
||||
.upload-zone{border:2px dashed #0f3460;border-radius:8px;padding:24px;text-align:center;
|
||||
color:#556;font-size:13px;cursor:pointer;transition:border-color .2s;margin-top:12px}
|
||||
.upload-zone:hover,.upload-zone.drag{border-color:#e94560;color:#8899aa}
|
||||
.upload-zone input{display:none}
|
||||
|
||||
.net-if{margin-bottom:12px}
|
||||
.net-if h4{font-size:12px;color:#7ec8e3;margin-bottom:6px}
|
||||
|
||||
.toast{position:fixed;bottom:20px;right:20px;background:#16213e;border:1px solid #0f3460;
|
||||
border-radius:8px;padding:10px 16px;font-size:12px;color:#c0d0e0;
|
||||
transform:translateY(80px);opacity:0;transition:all .3s;z-index:999}
|
||||
.toast.show{transform:translateY(0);opacity:1}
|
||||
.toast.err{border-color:#e94560}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hdr">
|
||||
<h1>NuttX Web Panel <span class="chip" id="chip">--</span></h1>
|
||||
<span class="uptime" id="uptime">Uptime: --</span>
|
||||
</div>
|
||||
|
||||
<nav id="nav">
|
||||
<a href="#home" class="act" data-p="home">Home</a>
|
||||
<a href="#terminal" data-p="terminal">Terminal</a>
|
||||
<a href="#files" data-p="files">Files</a>
|
||||
<a href="#network" data-p="network">Network</a>
|
||||
</nav>
|
||||
|
||||
<!-- HOME -->
|
||||
<div class="page vis" id="p-home">
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3><span class="ico">⚙</span> System</h3>
|
||||
<div class="row"><span class="lbl">Board</span><span class="val" id="si-board">--</span></div>
|
||||
<div class="row"><span class="lbl">OS</span><span class="val" id="si-os">NuttX</span></div>
|
||||
<div class="row"><span class="lbl">Version</span><span class="val" id="si-ver">--</span></div>
|
||||
<div class="row"><span class="lbl">Build</span><span class="val" id="si-build">--</span></div>
|
||||
<div class="row"><span class="lbl">Arch</span><span class="val" id="si-arch">--</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3><span class="ico">🔌</span> Network</h3>
|
||||
<div class="row"><span class="lbl">Interface</span><span class="val" id="si-ifname">--</span></div>
|
||||
<div class="row"><span class="lbl">IP</span><span class="val" id="si-ip">--</span></div>
|
||||
<div class="row"><span class="lbl">Mask</span><span class="val" id="si-mask">--</span></div>
|
||||
<div class="row"><span class="lbl">Gateway</span><span class="val" id="si-gw">--</span></div>
|
||||
<div class="row"><span class="lbl">MAC</span><span class="val" id="si-mac">--</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3><span class="ico">💾</span> Storage</h3>
|
||||
<div class="row"><span class="lbl">SmartFS</span><span class="val">/mnt</span></div>
|
||||
<div class="row"><span class="lbl">Web Root</span><span class="val">/data/www (ROMFS)</span></div>
|
||||
<div class="row"><span class="lbl">Files</span><span class="val" id="si-files">--</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3><span class="ico">⚡</span> Quick Actions</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;margin-top:4px">
|
||||
<button class="btn" onclick="showPage('terminal')">Open Terminal</button>
|
||||
<button class="btn btn-sec" onclick="showPage('files')">Manage Files</button>
|
||||
<button class="btn btn-sec" onclick="fetchSysInfo()">Refresh Info</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TERMINAL -->
|
||||
<div class="page" id="p-terminal">
|
||||
<div class="card" style="padding:12px">
|
||||
<h3><span class="ico">⌨</span> NSH Terminal</h3>
|
||||
<div class="fm-toolbar" style="margin-bottom:8px">
|
||||
<button class="btn btn-sec" id="term-connect" onclick="connectTerminal()">Connect</button>
|
||||
<button class="btn btn-sec" id="term-disc" onclick="disconnectTerminal()" style="display:none">Disconnect</button>
|
||||
<span id="term-status" style="font-size:11px;color:#8899aa;margin-left:8px">Disconnected</span>
|
||||
</div>
|
||||
<div class="term-wrap" id="term-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILES -->
|
||||
<div class="page" id="p-files">
|
||||
<div class="card" style="padding:12px">
|
||||
<h3><span class="ico">📁</span> File Manager — /mnt</h3>
|
||||
<div class="fm-toolbar">
|
||||
<span class="fm-path">/mnt</span>
|
||||
<button class="btn btn-sec" onclick="fetchFiles()">Refresh</button>
|
||||
</div>
|
||||
<div class="fm-list" id="fm-list">
|
||||
<div class="fm-empty">Loading file list…</div>
|
||||
</div>
|
||||
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('file-in').click()">
|
||||
<input type="file" id="file-in" onchange="uploadFile(this.files[0])">
|
||||
Drop file here or click to upload to /mnt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NETWORK -->
|
||||
<div class="page" id="p-network">
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3><span class="ico">🔌</span> <span id="net-ifname">--</span></h3>
|
||||
<div class="net-if">
|
||||
<div class="row"><span class="lbl">Status</span><span class="val"><span class="badge badge-ok">RUNNING</span></span></div>
|
||||
<div class="row"><span class="lbl">IP Address</span><span class="val" id="net-ip">--</span></div>
|
||||
<div class="row"><span class="lbl">Netmask</span><span class="val" id="net-mask">--</span></div>
|
||||
<div class="row"><span class="lbl">Gateway</span><span class="val" id="net-gw">--</span></div>
|
||||
<div class="row"><span class="lbl">MAC</span><span class="val" id="net-mac">--</span></div>
|
||||
<div class="row"><span class="lbl">MTU</span><span class="val" id="net-mtu">1500</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3><span class="ico">📜</span> DHCP</h3>
|
||||
<div class="row"><span class="lbl">Mode</span><span class="val">Client (auto)</span></div>
|
||||
<div class="row"><span class="lbl">Last Renew</span><span class="val" id="net-lease">--</span></div>
|
||||
<div style="margin-top:10px;display:flex;align-items:center;gap:10px">
|
||||
<button class="btn btn-sec" id="renew-btn" onclick="renewDhcp()">Renew Lease</button>
|
||||
<span id="renew-status" style="font-size:11px;color:#8899aa"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="/xterm.min.js"></script>
|
||||
<script>
|
||||
var term=null, termWs=null, termReady=false, termQueue=[], termDataSub=null;
|
||||
var uptimeSec=0, uptimeRef=0;
|
||||
function $(id){return document.getElementById(id)}
|
||||
function showPage(p){
|
||||
document.querySelectorAll('.page').forEach(function(e){e.classList.remove('vis')});
|
||||
document.querySelectorAll('nav a').forEach(function(e){e.classList.remove('act')});
|
||||
$('p-'+p).classList.add('vis');
|
||||
document.querySelector('nav a[data-p="'+p+'"]').classList.add('act');
|
||||
if(p==='terminal')initTerminal();
|
||||
}
|
||||
document.querySelectorAll('nav a').forEach(function(a){
|
||||
a.addEventListener('click',function(e){
|
||||
e.preventDefault();showPage(this.dataset.p);
|
||||
});
|
||||
});
|
||||
|
||||
function toast(msg,err){
|
||||
var t=$('toast');t.textContent=msg;
|
||||
t.className='toast'+(err?' err':'')+' show';
|
||||
setTimeout(function(){t.classList.remove('show')},3000);
|
||||
}
|
||||
|
||||
function fmtUptime(s){
|
||||
var d=Math.floor(s/86400);
|
||||
var h=Math.floor((s%86400)/3600);
|
||||
var m=Math.floor((s%3600)/60);
|
||||
var sec=s%60;
|
||||
var hh=('0'+h).slice(-2),mm=('0'+m).slice(-2),ss=('0'+sec).slice(-2);
|
||||
return d>0?(d+'d '+hh+':'+mm+':'+ss):(hh+':'+mm+':'+ss);
|
||||
}
|
||||
|
||||
setInterval(function(){
|
||||
if(!uptimeRef)return;
|
||||
var s=uptimeSec+Math.floor((Date.now()-uptimeRef)/1000);
|
||||
$('uptime').textContent='Uptime: '+fmtUptime(s);
|
||||
},1000);
|
||||
|
||||
function fetchSysInfo(){
|
||||
var x=new XMLHttpRequest();
|
||||
x.open('GET','/cgi-bin/sysinfo');
|
||||
x.onload=function(){
|
||||
if(x.status===200){
|
||||
try{
|
||||
var d=JSON.parse(x.responseText);
|
||||
if(d.version)$('si-ver').textContent=d.version;
|
||||
if(d.build)$('si-build').textContent=d.build;
|
||||
if(d.chip)$('chip').textContent=d.chip;
|
||||
if(d.board)$('si-board').textContent=d.board;
|
||||
if(d.ifname){
|
||||
$('si-ifname').textContent=d.ifname;
|
||||
$('net-ifname').textContent=d.ifname;
|
||||
}
|
||||
if(d.arch)$('si-arch').textContent=d.arch;
|
||||
if(d.ip){$('si-ip').textContent=d.ip;$('net-ip').textContent=d.ip;}
|
||||
if(d.mask){$('si-mask').textContent=d.mask;$('net-mask').textContent=d.mask;}
|
||||
if(d.gw){$('si-gw').textContent=d.gw;$('net-gw').textContent=d.gw;}
|
||||
if(d.mac){$('si-mac').textContent=d.mac;$('net-mac').textContent=d.mac;}
|
||||
if(d.uptime_sec!==undefined){
|
||||
uptimeSec=d.uptime_sec;
|
||||
uptimeRef=Date.now();
|
||||
$('uptime').textContent='Uptime: '+fmtUptime(d.uptime_sec);
|
||||
} else if(d.uptime){
|
||||
$('uptime').textContent='Uptime: '+d.uptime;
|
||||
}
|
||||
if(d.files!==undefined)$('si-files').textContent=d.files;
|
||||
toast('System info updated');
|
||||
}catch(e){toast('Parse error','err');}
|
||||
}
|
||||
};
|
||||
x.onerror=function(){};
|
||||
x.send();
|
||||
}
|
||||
|
||||
function fetchFiles(){
|
||||
var x=new XMLHttpRequest();
|
||||
x.open('GET','/cgi-bin/files');
|
||||
x.onload=function(){
|
||||
if(x.status===200){
|
||||
try{
|
||||
var d=JSON.parse(x.responseText);
|
||||
renderFiles(d.files||[]);
|
||||
}catch(e){renderFiles([]);}
|
||||
}
|
||||
};
|
||||
x.onerror=function(){
|
||||
$('fm-list').innerHTML='<div class="fm-empty">Could not load files (CGI not ready)</div>';
|
||||
};
|
||||
x.send();
|
||||
}
|
||||
|
||||
function renderFiles(files){
|
||||
var el=$('fm-list');
|
||||
if(!files.length){el.innerHTML='<div class="fm-empty">No files in /mnt</div>';return;}
|
||||
var h='';
|
||||
for(var i=0;i<files.length;i++){
|
||||
var f=files[i];
|
||||
h+='<div class="fm-item">'
|
||||
+'<span class="name">'+esc(f.name)+'</span>'
|
||||
+'<span class="sz">'+(f.size||'--')+'</span>'
|
||||
+'<span class="acts">';
|
||||
if(f.name.match(/\.py$/i))
|
||||
h+='<button onclick="runScript(\''+esc(f.name)+'\')">Run</button>';
|
||||
h+='<button onclick="deleteFile(\''+esc(f.name)+'\')">Del</button>'
|
||||
+'</span></div>';
|
||||
}
|
||||
el.innerHTML=h;
|
||||
}
|
||||
|
||||
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');}
|
||||
|
||||
function uploadFile(file){
|
||||
if(!file)return;
|
||||
toast('Uploading '+file.name+'...');
|
||||
var form=new FormData();
|
||||
form.append('file',file);
|
||||
var x=new XMLHttpRequest();
|
||||
x.open('POST','/cgi-bin/upload');
|
||||
x.onload=function(){
|
||||
if(x.status===200){toast('Uploaded '+file.name);fetchFiles();}
|
||||
else toast('Upload failed: '+x.status,true);
|
||||
};
|
||||
x.onerror=function(){toast('Upload failed (CGI not ready)',true);};
|
||||
x.send(form);
|
||||
}
|
||||
|
||||
function deleteFile(name){
|
||||
if(!confirm('Delete '+name+'?'))return;
|
||||
var x=new XMLHttpRequest();
|
||||
x.open('POST','/cgi-bin/files?action=delete&name='+encodeURIComponent(name));
|
||||
x.onload=function(){
|
||||
if(x.status===200){toast('Deleted '+name);fetchFiles();}
|
||||
else toast('Delete failed',true);
|
||||
};
|
||||
x.send();
|
||||
}
|
||||
|
||||
function initTerminal(){
|
||||
if(term)return;
|
||||
term=new Terminal({cursorBlink:true,fontSize:13,theme:{background:'#0d1117',foreground:'#c0d0e0'}});
|
||||
term.open($('term-container'));
|
||||
term.writeln('NuttX Web Terminal - click Connect to start NSH session');
|
||||
termDataSub=term.onData(function(data){
|
||||
if(termWs&&termWs.readyState===WebSocket.OPEN)termWs.send(data);
|
||||
else termQueue.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
function setTermStatus(s,ok){
|
||||
$('term-status').textContent=s;
|
||||
$('term-status').style.color=ok?'#4ade80':'#8899aa';
|
||||
}
|
||||
|
||||
function connectTerminal(){
|
||||
initTerminal();
|
||||
if(termWs&&termWs.readyState===WebSocket.OPEN)return;
|
||||
var host=window.location.hostname||'127.0.0.1';
|
||||
var url='ws://'+host+':8080';
|
||||
setTermStatus('Connecting...',false);
|
||||
termWs=new WebSocket(url);
|
||||
termWs.onopen=function(){
|
||||
termReady=true;
|
||||
setTermStatus('Connected',true);
|
||||
$('term-connect').style.display='none';
|
||||
$('term-disc').style.display='';
|
||||
while(termQueue.length)termWs.send(termQueue.shift());
|
||||
};
|
||||
termWs.onmessage=function(ev){term.write(ev.data);};
|
||||
termWs.onclose=function(){
|
||||
termReady=false;
|
||||
setTermStatus('Disconnected',false);
|
||||
$('term-connect').style.display='';
|
||||
$('term-disc').style.display='none';
|
||||
termWs=null;
|
||||
};
|
||||
termWs.onerror=function(){
|
||||
toast('WebSocket connection failed',true);
|
||||
setTermStatus('Error',false);
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectTerminal(){
|
||||
if(termWs)termWs.close();
|
||||
}
|
||||
|
||||
function sendTerminalCmd(cmd){
|
||||
showPage('terminal');
|
||||
if(!term)initTerminal();
|
||||
if(!termWs||termWs.readyState!==WebSocket.OPEN){
|
||||
connectTerminal();
|
||||
setTimeout(function(){sendTerminalCmd(cmd);},800);
|
||||
return;
|
||||
}
|
||||
termWs.send(cmd);
|
||||
}
|
||||
|
||||
function runScript(name){
|
||||
var cmd='python /mnt/'+name;
|
||||
sendTerminalCmd(cmd+'\r');
|
||||
toast('Running: '+cmd);
|
||||
}
|
||||
|
||||
function renewDhcp(){
|
||||
var btn=$('renew-btn'), st=$('renew-status');
|
||||
btn.disabled=true;
|
||||
st.textContent='Renewing...';
|
||||
st.style.color='#fbbf24';
|
||||
var x=new XMLHttpRequest();
|
||||
x.open('GET','/cgi-bin/dhcprenew');
|
||||
x.timeout=30000;
|
||||
x.onload=function(){
|
||||
btn.disabled=false;
|
||||
try{
|
||||
var d=JSON.parse(x.responseText);
|
||||
if(d.status==='ok'){
|
||||
var now=new Date();
|
||||
var ts=('0'+now.getHours()).slice(-2)+':'+('0'+now.getMinutes()).slice(-2)+':'+('0'+now.getSeconds()).slice(-2);
|
||||
$('net-lease').textContent='Renewed at '+ts;
|
||||
st.textContent='OK';
|
||||
st.style.color='#4ade80';
|
||||
toast('DHCP lease renewed');
|
||||
setTimeout(fetchSysInfo,1000);
|
||||
} else {
|
||||
st.textContent='Failed (code '+d.code+')';
|
||||
st.style.color='#e94560';
|
||||
toast('DHCP renew failed',true);
|
||||
}
|
||||
}catch(e){
|
||||
st.textContent='Error';
|
||||
st.style.color='#e94560';
|
||||
toast('DHCP renew error',true);
|
||||
}
|
||||
setTimeout(function(){st.textContent='';},5000);
|
||||
};
|
||||
x.onerror=x.ontimeout=function(){
|
||||
btn.disabled=false;
|
||||
st.textContent='Timeout';
|
||||
st.style.color='#e94560';
|
||||
toast('DHCP renew timed out',true);
|
||||
};
|
||||
x.send();
|
||||
}
|
||||
|
||||
var uz=$('upload-zone');
|
||||
uz.addEventListener('dragover',function(e){e.preventDefault();uz.classList.add('drag');});
|
||||
uz.addEventListener('dragleave',function(){uz.classList.remove('drag');});
|
||||
uz.addEventListener('drop',function(e){
|
||||
e.preventDefault();uz.classList.remove('drag');
|
||||
if(e.dataTransfer.files.length)uploadFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
|
||||
fetchSysInfo();
|
||||
fetchFiles();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
218
examples/webpanel/content/www/xterm.css
Normal file
218
examples/webpanel/content/www/xterm.css
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
8
examples/webpanel/content/www/xterm.min.js
vendored
Normal file
8
examples/webpanel/content/www/xterm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
207
examples/webpanel/webpanel_main.c
Normal file
207
examples/webpanel/webpanel_main.c
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/webpanel_main.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <sys/mount.h>
|
||||
#include <sys/boardctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <nuttx/drivers/ramdisk.h>
|
||||
|
||||
#include "netutils/thttpd.h"
|
||||
#include "fsutils/mksmartfs.h"
|
||||
#include "ws_terminal.h"
|
||||
|
||||
/****************************************************************************
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
#define SECTORSIZE 64
|
||||
#define NSECTORS(b) (((b) + SECTORSIZE - 1) / SECTORSIZE)
|
||||
#define WEBPANEL_RAMDISK_MINOR 2
|
||||
#define ROMFSDEV "/dev/ram2"
|
||||
|
||||
#define ROMFS_MOUNTPT "/data/tmp_romfs"
|
||||
#define BINFS_MOUNTPT "/data/tmp_binfs"
|
||||
#define BINFS_PREFIX "cgi-bin"
|
||||
#define UNIONFS_MOUNTPT CONFIG_THTTPD_PATH
|
||||
|
||||
#define SMARTFS_DEV "/dev/smart0"
|
||||
#define SMARTFS_MNT "/mnt"
|
||||
|
||||
/****************************************************************************
|
||||
* External References
|
||||
****************************************************************************/
|
||||
|
||||
extern const unsigned char webpanel_romfs_img[];
|
||||
extern const unsigned int webpanel_romfs_img_len;
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: main
|
||||
*
|
||||
* Description:
|
||||
* Initialize WebPanel filesystem mounts, start the websocket daemon,
|
||||
* then launch the THTTPD server.
|
||||
*
|
||||
* Input Parameters:
|
||||
* argc - Number of arguments.
|
||||
* argv - Argument vector.
|
||||
*
|
||||
* Returned Value:
|
||||
* EXIT_SUCCESS on success; EXIT_FAILURE on setup error.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int main(int argc, FAR char *argv[])
|
||||
{
|
||||
struct boardioc_romdisk_s desc;
|
||||
char *thttpd_argv = "thttpd";
|
||||
struct statfs check_fs;
|
||||
struct statfs sfs;
|
||||
int ret;
|
||||
|
||||
/* Prevent duplicate startup (e.g. rcS runs again in child NSH) */
|
||||
|
||||
if (statfs(UNIONFS_MOUNTPT, &check_fs) == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Register the ROMFS image as a ramdisk */
|
||||
|
||||
printf("WebPanel: Registering ROMFS ramdisk\n");
|
||||
|
||||
desc.minor = WEBPANEL_RAMDISK_MINOR;
|
||||
desc.nsectors = NSECTORS(webpanel_romfs_img_len);
|
||||
desc.sectsize = SECTORSIZE;
|
||||
desc.image = (FAR uint8_t *)webpanel_romfs_img;
|
||||
|
||||
ret = boardctl(BOARDIOC_ROMDISK, (uintptr_t)&desc);
|
||||
if (ret < 0 && errno != EEXIST)
|
||||
{
|
||||
printf("WebPanel: ERROR romdisk_register failed: %d\n", ret);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Create mount point directories */
|
||||
|
||||
mkdir("/data", 0777);
|
||||
mkdir(ROMFS_MOUNTPT, 0777);
|
||||
mkdir(BINFS_MOUNTPT, 0777);
|
||||
|
||||
/* Mount ROMFS at temp location */
|
||||
|
||||
printf("WebPanel: Mounting ROMFS at %s\n", ROMFS_MOUNTPT);
|
||||
|
||||
ret = mount(ROMFSDEV, ROMFS_MOUNTPT, "romfs", MS_RDONLY, NULL);
|
||||
if (ret < 0 && errno != EBUSY)
|
||||
{
|
||||
printf("WebPanel: ERROR mount ROMFS failed: %d\n", errno);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Mount BINFS at temp location (exposes built-in apps as CGI) */
|
||||
|
||||
printf("WebPanel: Mounting BINFS at %s\n", BINFS_MOUNTPT);
|
||||
|
||||
ret = mount(NULL, BINFS_MOUNTPT, "binfs", MS_RDONLY, NULL);
|
||||
if (ret < 0 && errno != EBUSY)
|
||||
{
|
||||
printf("WebPanel: ERROR mount BINFS failed: %d\n", errno);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Create UNIONFS: ROMFS content + BINFS under cgi-bin/ prefix */
|
||||
|
||||
printf("WebPanel: Creating UNIONFS at %s\n", UNIONFS_MOUNTPT);
|
||||
|
||||
ret = mount(NULL, UNIONFS_MOUNTPT, "unionfs", 0,
|
||||
"fspath1=" ROMFS_MOUNTPT ",prefix1="
|
||||
",fspath2=" BINFS_MOUNTPT ",prefix2=" BINFS_PREFIX);
|
||||
if (ret < 0 && errno != EBUSY)
|
||||
{
|
||||
printf("WebPanel: ERROR mount UNIONFS failed: %d\n", errno);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/* Ensure SmartFS is formatted and mounted at /mnt for user files */
|
||||
|
||||
if (statfs(SMARTFS_MNT, &sfs) != 0)
|
||||
{
|
||||
printf("WebPanel: SmartFS not mounted, formatting %s\n",
|
||||
SMARTFS_DEV);
|
||||
ret = mksmartfs(SMARTFS_DEV, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("WebPanel: WARNING mksmartfs failed: %d\n", errno);
|
||||
}
|
||||
else
|
||||
{
|
||||
mkdir(SMARTFS_MNT, 0777);
|
||||
ret = mount(SMARTFS_DEV, SMARTFS_MNT, "smartfs", 0, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("WebPanel: WARNING mount SmartFS failed: %d\n",
|
||||
errno);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("WebPanel: SmartFS mounted at %s\n", SMARTFS_MNT);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("WebPanel: SmartFS already mounted at %s\n", SMARTFS_MNT);
|
||||
}
|
||||
|
||||
/* Start WebSocket terminal server */
|
||||
|
||||
ret = ws_terminal_start();
|
||||
if (ret < 0)
|
||||
{
|
||||
printf("WebPanel: WARNING WebSocket server failed to start\n");
|
||||
}
|
||||
|
||||
/* Start THTTPD */
|
||||
|
||||
printf("WebPanel: Starting THTTPD (serving from %s)\n", UNIONFS_MOUNTPT);
|
||||
fflush(stdout);
|
||||
|
||||
thttpd_main(1, &thttpd_argv);
|
||||
|
||||
printf("WebPanel: THTTPD terminated\n");
|
||||
return 0;
|
||||
}
|
||||
350
examples/webpanel/ws_terminal.c
Normal file
350
examples/webpanel/ws_terminal.c
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/ws_terminal.c
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#include <nuttx/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sched.h>
|
||||
#include <spawn.h>
|
||||
|
||||
#include <libwebsockets.h>
|
||||
|
||||
#include "ws_terminal.h"
|
||||
|
||||
/****************************************************************************
|
||||
* Pre-processor Definitions
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_PORT
|
||||
# define CONFIG_EXAMPLES_WEBPANEL_WS_PORT 8080
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY
|
||||
# define CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY 100
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE
|
||||
# define CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE 8192
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE
|
||||
# define CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE 4096
|
||||
#endif
|
||||
|
||||
#define WS_IOBUF_SIZE 512
|
||||
|
||||
/****************************************************************************
|
||||
* Private Types
|
||||
****************************************************************************/
|
||||
|
||||
struct ws_session
|
||||
{
|
||||
int masterfd;
|
||||
pid_t nshpid;
|
||||
unsigned char txbuf[LWS_PRE + WS_IOBUF_SIZE];
|
||||
size_t txlen;
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
* Private Function Prototypes
|
||||
****************************************************************************/
|
||||
|
||||
static int ws_spawn_nsh(int masterfd, pid_t *pid);
|
||||
static int ws_callback(struct lws *wsi,
|
||||
enum lws_callback_reasons reason,
|
||||
void *user, void *in, size_t len);
|
||||
static int ws_daemon(int argc, FAR char *argv[]);
|
||||
|
||||
/****************************************************************************
|
||||
* Private Data
|
||||
****************************************************************************/
|
||||
|
||||
static const struct lws_protocols g_protocols[] =
|
||||
{
|
||||
{
|
||||
"",
|
||||
ws_callback,
|
||||
sizeof(struct ws_session),
|
||||
WS_IOBUF_SIZE,
|
||||
0, NULL, 0
|
||||
},
|
||||
LWS_PROTOCOL_LIST_TERM
|
||||
};
|
||||
|
||||
/****************************************************************************
|
||||
* Private Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: ws_spawn_nsh
|
||||
*
|
||||
* Description:
|
||||
* Spawn an NSH instance attached to a PTY slave.
|
||||
*
|
||||
* Input Parameters:
|
||||
* masterfd - PTY master descriptor.
|
||||
* pid - Location to receive spawned task PID.
|
||||
*
|
||||
* Returned Value:
|
||||
* Zero on success; negated errno on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int ws_spawn_nsh(int masterfd, pid_t *pid)
|
||||
{
|
||||
char slavepath[32];
|
||||
FAR char *nshargv[] =
|
||||
{
|
||||
"nsh", NULL
|
||||
};
|
||||
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawnattr_t attr;
|
||||
struct sched_param param;
|
||||
int ret;
|
||||
|
||||
ret = grantpt(masterfd);
|
||||
if (ret < 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = unlockpt(masterfd);
|
||||
if (ret < 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = ptsname_r(masterfd, slavepath, sizeof(slavepath));
|
||||
if (ret < 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
posix_spawn_file_actions_addopen(&actions, 0, slavepath,
|
||||
O_RDWR, 0);
|
||||
posix_spawn_file_actions_adddup2(&actions, 0, 1);
|
||||
posix_spawn_file_actions_adddup2(&actions, 0, 2);
|
||||
|
||||
posix_spawnattr_init(&attr);
|
||||
param.sched_priority = CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY;
|
||||
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
|
||||
posix_spawnattr_setschedparam(&attr, ¶m);
|
||||
posix_spawnattr_setstacksize(&attr,
|
||||
CONFIG_EXAMPLES_WEBPANEL_WS_NSH_STACKSIZE);
|
||||
|
||||
ret = posix_spawn(pid, "nsh", &actions, &attr, nshargv, NULL);
|
||||
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
posix_spawnattr_destroy(&attr);
|
||||
|
||||
return ret == 0 ? 0 : -ret;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: ws_callback
|
||||
*
|
||||
* Description:
|
||||
* libwebsockets protocol callback for the NSH terminal relay.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int ws_callback(struct lws *wsi,
|
||||
enum lws_callback_reasons reason,
|
||||
void *user, void *in, size_t len)
|
||||
{
|
||||
struct ws_session *sess = (struct ws_session *)user;
|
||||
ssize_t n;
|
||||
|
||||
switch (reason)
|
||||
{
|
||||
case LWS_CALLBACK_ESTABLISHED:
|
||||
sess->masterfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
|
||||
if (sess->masterfd < 0)
|
||||
{
|
||||
printf("ws_terminal: failed to open PTY\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
sess->nshpid = -1;
|
||||
if (ws_spawn_nsh(sess->masterfd, &sess->nshpid) < 0)
|
||||
{
|
||||
printf("ws_terminal: failed to spawn NSH\n");
|
||||
close(sess->masterfd);
|
||||
sess->masterfd = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Make PTY master non-blocking for polling */
|
||||
|
||||
fcntl(sess->masterfd, F_SETFL,
|
||||
fcntl(sess->masterfd, F_GETFL) | O_NONBLOCK);
|
||||
|
||||
sess->txlen = 0;
|
||||
printf("ws_terminal: client connected\n");
|
||||
|
||||
lws_set_timer_usecs(wsi, 50 * LWS_USEC_PER_SEC / 1000);
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_RECEIVE:
|
||||
if (sess->masterfd >= 0 && len > 0)
|
||||
{
|
||||
write(sess->masterfd, in, len);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_SERVER_WRITEABLE:
|
||||
if (sess->masterfd < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
n = read(sess->masterfd,
|
||||
&sess->txbuf[LWS_PRE],
|
||||
WS_IOBUF_SIZE);
|
||||
if (n > 0)
|
||||
{
|
||||
lws_write(wsi, &sess->txbuf[LWS_PRE], n, LWS_WRITE_TEXT);
|
||||
lws_set_timer_usecs(wsi, 10 * LWS_USEC_PER_SEC / 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
lws_set_timer_usecs(wsi, 50 * LWS_USEC_PER_SEC / 1000);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_TIMER:
|
||||
lws_callback_on_writable(wsi);
|
||||
break;
|
||||
|
||||
case LWS_CALLBACK_CLOSED:
|
||||
printf("ws_terminal: client disconnected\n");
|
||||
if (sess->nshpid > 0)
|
||||
{
|
||||
task_delete(sess->nshpid);
|
||||
sess->nshpid = -1;
|
||||
}
|
||||
|
||||
if (sess->masterfd >= 0)
|
||||
{
|
||||
close(sess->masterfd);
|
||||
sess->masterfd = -1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: ws_daemon
|
||||
*
|
||||
* Description:
|
||||
* WebSocket terminal daemon using libwebsockets.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static int ws_daemon(int argc, FAR char *argv[])
|
||||
{
|
||||
struct lws_context_creation_info info;
|
||||
struct lws_context *context;
|
||||
int n;
|
||||
|
||||
lws_set_log_level(LLL_ERR | LLL_WARN, NULL);
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.port = CONFIG_EXAMPLES_WEBPANEL_WS_PORT;
|
||||
info.protocols = g_protocols;
|
||||
info.vhost_name = "localhost";
|
||||
info.options = 0;
|
||||
|
||||
context = lws_create_context(&info);
|
||||
if (context == NULL)
|
||||
{
|
||||
printf("ws_terminal: ERROR creating lws context\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
printf("WebPanel: WebSocket terminal listening on port %d\n",
|
||||
CONFIG_EXAMPLES_WEBPANEL_WS_PORT);
|
||||
|
||||
n = 0;
|
||||
while (n >= 0)
|
||||
{
|
||||
n = lws_service(context, 100);
|
||||
}
|
||||
|
||||
lws_context_destroy(context);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Public Functions
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: ws_terminal_start
|
||||
*
|
||||
* Description:
|
||||
* Start the websocket terminal daemon task.
|
||||
*
|
||||
* Input Parameters:
|
||||
* None.
|
||||
*
|
||||
* Returned Value:
|
||||
* Task PID on success; negated errno value on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int ws_terminal_start(void)
|
||||
{
|
||||
int pid;
|
||||
|
||||
pid = task_create("ws_daemon",
|
||||
CONFIG_EXAMPLES_WEBPANEL_WS_PRIORITY,
|
||||
CONFIG_EXAMPLES_WEBPANEL_WS_STACKSIZE,
|
||||
ws_daemon, NULL);
|
||||
if (pid < 0)
|
||||
{
|
||||
printf("WebPanel: ERROR failed to start WebSocket server: %d\n",
|
||||
errno);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
return pid;
|
||||
}
|
||||
63
examples/webpanel/ws_terminal.h
Normal file
63
examples/webpanel/ws_terminal.h
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/****************************************************************************
|
||||
* apps/examples/webpanel/ws_terminal.h
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership. The
|
||||
* ASF licenses this file to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the
|
||||
* License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H
|
||||
#define __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H
|
||||
|
||||
/****************************************************************************
|
||||
* Included Files
|
||||
****************************************************************************/
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define EXTERN extern "C"
|
||||
extern "C"
|
||||
{
|
||||
#else
|
||||
#define EXTERN extern
|
||||
#endif
|
||||
|
||||
/****************************************************************************
|
||||
* Public Function Prototypes
|
||||
****************************************************************************/
|
||||
|
||||
/****************************************************************************
|
||||
* Name: ws_terminal_start
|
||||
*
|
||||
* Description:
|
||||
* Start the websocket terminal daemon task.
|
||||
*
|
||||
* Input Parameters:
|
||||
* None.
|
||||
*
|
||||
* Returned Value:
|
||||
* Task PID on success; negated errno value on failure.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
int ws_terminal_start(void);
|
||||
|
||||
#undef EXTERN
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __APPS_EXAMPLES_WEBPANEL_WS_TERMINAL_H */
|
||||
Loading…
Add table
Add a link
Reference in a new issue