Tired of scrounging for a USB cable to access your NetBurner serial console? No longer!
A common problem when developing embedded applications is when you’re testing, deploying, or maintaining the program on a device or in the field and physical access is limited. USB cables only stretch so far!
Our team felt this limitation ourselves and with a name like NetBurner you’d expect a fully-network-enabled stack to be possible out of the box, right?
So, over the holidays we whipped up a Remote Console powered by WebSockets, accessible over the same HTTP port used by normal NetBurner applications and customizable to your needs.
It’ll be part of the upcoming NNDK release, but we’re also making the code available on its own so you can backport this feature to your NNDK 3.4 apps.
The Code
You can access or browse the files and instructions for this blog post via Github if you prefer! Downloading a ZIP of the repository via the green Code download button at the top right might be easier for you than copy-pasting from this post, your choice.
This project was developed for NetBurner’s NNDK 3.4+ — older versions aren’t guaranteed to be compatible.
All code in this blog post and the Github repository are Copyright NetBurner, Inc, and may only be executed on NetBurner provided hardware. See LICENSE.md in the repository for more details.
Backporting
- You can skip this section and scroll down to Add Remote Console to Project if you’re living in the future and already have an NNDK version higher than 3.4.x.
- Create these files under the specified folders in
C:\NetBurner
or whatever your NNDK_ROOT is:
// nbrtos\include\remoteconsole.h
#ifndef NB_RMTCONSOLE_H
#define NB_RMTCONSOLE_H
void EnableRemoteConsole();
#endif
// nbrtos\source\remoteconsole.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern http_wshandler *TheWSHandler;
int ws_fd = -1;
void BadRequestResponse(int sock, PCSTR url, PCSTR data);
void NotAvailableResponse(int sock, PCSTR url);
using namespace NB;
int httpstricmp(PCSTR s1, PCSTR sisupper2);
static int ShimFd;
void ShimCallBack(int fd ,FDChangeType ct,void *p);
int MyDoWSUpgrade(HTTP_Request *req, int sock, PSTR url, PSTR rxb)
{
if (httpstricmp(url, "/STDIO"))
{
if (ws_fd < 0)
{
int rv = WSUpgrade(req, sock);
if (rv >= 0)
{
ws_fd = rv;
NB::WebSocket::ws_setoption(ws_fd, WS_SO_TEXT);
RegisterFDCallBack(ws_fd,ShimCallBack,0);
return 2;
}
else
{
return 0;
}
}
return 0;
}
NotFoundResponse(sock, url);
return 0;
}
int shim_fd;
IoExpandStruct shim_io;
int OldStdio[3];
int ShimRead(int fd, char *buf, int nbytes)
{
int rv=0;
if (
(!dataavail(OldStdio[0])) &&
((ws_fd<0) || (!dataavail(ws_fd)))
) {
//Do a select
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
select(FD_SETSIZE, &read_fds,(fd_set *)0,(fd_set *)0,0);
}
if((ws_fd>0)&& dataavail(ws_fd)) rv=read(ws_fd,buf,nbytes);
else
if(dataavail(OldStdio[0]))rv= read(OldStdio[0],buf,nbytes);
bool da=((ws_fd>0)&& dataavail(ws_fd));
da|=dataavail(OldStdio[0]);
if(da)
SetDataAvail(fd);
else
ClrDataAvail(fd);
return rv;
}
int ShimWrite(int fd, const char *buf, int nbytes)
{
write(OldStdio[1],buf,nbytes);
if((ws_fd>0)&& (writeavail(ws_fd))) write(ws_fd,buf,nbytes);
return nbytes;
}
int ShimClose(int fd)
{
return 1;
}
int ShimPeek (int fd, char *buf)
{
return 0;
}
void ShimCallBack(int fd ,FDChangeType ct,void *p)
{
if(dataavail(fd)) SetDataAvail(shim_fd);
if(fd==ws_fd) {
switch(ct) {
case eReadSet: break;
case eWriteSet: break;
case eErrorSet: {
int ows=ws_fd;
ws_fd=-1;
close(ows);
}
}
}
}
void InitStdioShim()
{
shim_io.read=ShimRead;
shim_io.write=ShimWrite;
shim_io.close=ShimClose;
shim_io.peek=ShimPeek;
shim_fd=GetExtraFD(0,&shim_io);
SetWriteAvail(shim_fd);
OldStdio[0]=ReplaceStdio(0, shim_fd);
OldStdio[1]=ReplaceStdio(1, shim_fd);
OldStdio[2]=ReplaceStdio(2, shim_fd);
RegisterFDCallBack(OldStdio[0],ShimCallBack,0);
}
int ServeValidResponse(int sock, HTTP_Request &pd)
{
writestring(sock, "HTTP/1.0 200 OK\r\nPragma: no-cache\r\nContent-Type: application/json\r\n\r\n");
if(ws_fd>0)
writestring(sock, "{\"Valid\":true}");
else
writestring(sock, "{\"Valid\":false}");
return 1;
}
extern const unsigned long console_html_size;
extern const unsigned char console_html_data[];
int ServeConsoleHtml(int sock,HTTP_Request &pd)
{
SendHTMLHeader(sock);
writeall(sock,(const char *)console_html_data,console_html_size);
close(sock);
return 1;
}
CallBackFunctionPageHandler ValidWS("ValidWS.json", ServeValidResponse);
CallBackFunctionPageHandler ServeConsole("console.html", ServeConsoleHtml);
void EnableRemoteConsole()
{
InitStdioShim();
TheWSHandler = MyDoWSUpgrade;
}
2. Add these lines to nbrtos\source\library.mak
: Make sure the second line is indented with a tab, not spaces.
# nbrtos\source\library.mak
$(OBJDIR)/CONSOLE_html.cpp: $(subst $(NNDK_ROOT)/,,$(LIB_PATH))/console.html
compfile $< CONSOLE_html_data CONSOLE_html_size $@
#...
LIB_CPP_SRC += $(addprefix $(OBJDIR)/, CONSOLE_html.cpp)
Add remote console to project
If you prefer, you can also use example-console/main.cpp
from the Github repo in a new NetBurner project or copy-pasted as an example.
Add the new include to the top of your project:#include <remoteconsole.h>
Add to your UserMain function if not already:
// UserMain()
init(); // Initialize network stack
// Start web server, default port 80, required for remote console
StartHttp();
WaitForActiveNetwork(TICKS_PER_SECOND * 5); // Wait for DHCP address
// Enable remote HTTP "serial" console. Not PW protected.
EnableRemoteConsole();
- Clean your NetBurner System Library (
make -j20 clean-nblibs
) - Run As NetBurner Application (
make load
) - Once uploaded to your NetBurner device, browse to /console.html (all lowercase) at the device’s IP (i.e. http://192.168.1.100/console.html)
- Any stdout like
getchar()
‘s output orprintf()
will be displayed on the console. Stdin is available viacharavail()
andgetchar()
. You can implement your own I/O or continue on for a full-fledged example.
Adding custom I/O handling
example-console/main.cpp
in the Github repo if you prefer.
We’ve implemented “time” and “reboot” commands, as well as debug output every 5 seconds, but you can customize as desired. Obviously this console page will be available without a username or password or encryption to anyone able to access your device’s webpage, so either only use this carefully for debugging/development on trusted networks or implement your own security best practices for access control. We suggest being very careful about handling what’s essentially a web-connected stdin
and allowing only extremely limited commands/inputs. We’ve avoided storing over 100 characters in the command buffer for example. Restricting access to HTTPS only and requiring a password to do sensitive things like reboot would be good next steps.
When done, your /console.html
should look like this:
// main.cpp
#include
#include
#include
#include
#include
#include
#include
#define SHUTDOWN_CUSTOM_REBOOT_REASON 100
void StoreCmdBuf(NBString &s, char c) {
// 100-char command limit for safety
if (s.length() < 100)
s += c;
}
void ExecuteCmdBuf(NBString &s) {
if (s == "time"){
printf("> Uptime is %lu seconds.\r\n",Secs);
} else if (s == "reboot") {
if( NBApproveShutdown(SHUTDOWN_CUSTOM_REBOOT_REASON))
{
printf("> Rebooting.\r\n");
OSTimeDly(TICKS_PER_SECOND * 5);
ForceReboot();
}
} else {
printf("\r\n> HELP:\r\n> (You typed [%s])\r\n", s.c_str());
printf("> time: get time\r\n> reboot: reboot\r\n");
}
}
void ClearCmdBuf(NBString &s) {
s.clear();
}
void OutputTask(void * pd)
{
while(1) {
iprintf("> Tick at %lu\r\n",Secs);
OSTimeDly(TICKS_PER_SECOND * 5); // delay 5 seconds
}
}
/*-----------------------------------------------------------------------------
* UserMain
*----------------------------------------------------------------------------*/
void UserMain(void * pd)
{
NBString cmdBuf; // store user-entered commands in a buffer
init(); // Initialize network stack
//Enable system diagnostics. Probably should remove for production code.
EnableSystemDiagnostics();
StartHttp(); // Start web server, default port 80
WaitForActiveNetwork(TICKS_PER_SECOND * 5); // Wait for DHCP address
// Enable console.html piped to stdin/stdout
EnableRemoteConsole();
// Make a background task for outputting some text without blocking
OSSimpleTaskCreatewName(OutputTask,20,"Output");
// Main app loop
while (1)
{
char c=getchar(); // getchar will block and also echo each char to stdout
if(c=='\n') { // newline executes command and clears buffer
ExecuteCmdBuf(cmdBuf);
ClearCmdBuf(cmdBuf);
} else { // all other chars add to buffer
StoreCmdBuf(cmdBuf,c);
}
}
}
Changing the console HTML
If the default example console HTML/Javascript isn’t quite to your liking, you can edit the console.html
file we put in the nbrtos folder above. It just needs to be compiled into console_html.cpp
afterwards for upload to the device.
If you encounter issues with the console.html not updating, run compfile console.html console_html_data console_html_size console_html.cpp
manually in your terminal after making any HTML changes. Try to keep the total HTML file size small to avoid issues.
Wrap-up
We hope you enjoy this powerful new ability to manage, monitor, develop and debug your NetBurner projects over the network without cables!
Remember that with great power comes great responsibility, think thrice before exposing network-enabled tools to semi-public or untrusted networks or the Internet! If using these remote access/control features in a production or sensitive environment, please follow OWASP best practices or other industry infosec standards.