Merge pull request #73 from tyfiero/Fix-captive-portal-and-tunneling-with-ngrok

Default to ngrok, fix portal, fix tunneling
pull/75/head
killian 10 months ago committed by GitHub
commit 18d7ff6b43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -19,7 +19,7 @@ int server_port = 8000;
// Pre reading on the fundamentals of captive portals https://textslashplain.com/2022/06/24/captive-portals/ // Pre reading on the fundamentals of captive portals https://textslashplain.com/2022/06/24/captive-portals/
const char *ssid = "captive"; // FYI The SSID can't have a space in it. const char *ssid = "01-Light"; // FYI The SSID can't have a space in it.
// const char * password = "12345678"; //Atleast 8 chars // const char * password = "12345678"; //Atleast 8 chars
const char *password = NULL; // no password const char *password = NULL; // no password
@ -38,7 +38,21 @@ const int kNetworkDelay = 1000;
String generateHTMLWithSSIDs() String generateHTMLWithSSIDs()
{ {
String html = "<!DOCTYPE html><html><body><h2>Select Wi-Fi Network</h2><form action='/submit' method='POST'><label for='ssid'>SSID:</label><select id='ssid' name='ssid'>"; String html = "<!DOCTYPE html><html><head><title>WiFi Setup</title>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<style> *{box-sizing: border-box;} body {background-color: #fff; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: Helvetica, sans-serif;} "
"h1 {color: black; text-align: center; margin: 40px;}"
"form {display: flex; flex-direction: column; align-items: center;} "
"input[type='text'], input[type='password'], select {margin-bottom: 10px; font-size: 16px; padding: 8px;} "
"input[type='submit'] {background-color: black; color: white; border: none; padding: 10px 20px; cursor: pointer; font-size: 16px; margin-top: 28px;} "
"input[type='submit']:hover {background-color: #333; }#otherSSID {display: none;}</style>"
"<script>function checkSSID(value) {"
"var otherSSID = document.getElementById('otherSSID');"
"if(value === 'OTHER') { otherSSID.style.display = 'block'; } else { otherSSID.style.display = 'none'; }}"
"</script></head>"
"<body><h1>01 Light</h1><form action='/submit' method='post'>"
"<div><div><label for='ssid'>WiFi Network Name:</label><br><br>"
"<select id='ssid' name='ssid' onchange='checkSSID(this.value);'>";
int n = WiFi.scanComplete(); int n = WiFi.scanComplete();
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
@ -46,55 +60,122 @@ String generateHTMLWithSSIDs()
html += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + "</option>"; html += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + "</option>";
} }
html += "</select><br><label for='password'>Password:</label><input type='password' id='password' name='password'><br><input type='submit' value='Connect'></form></body></html>"; html += "<option value='OTHER'>Other</option></select><br><br><input type='text' id='otherSSID' name='otherSSID' placeholder='Enter WiFi Name'><br></div><div><label for='password'>Password:</label><br><br>"
"<input type='password' id='password' name='password' ><br></div></div>"
"<input type='submit' value='Connect'/></form></body></html>";
return html; return html;
} }
const char index_html[] PROGMEM = R"=====( const char post_connected_html[] PROGMEM = R"=====(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>WiFi Setup</title> <title>01OS Setup</title>
<style> <style>
body {background-color:#06cc13;}
h1 {color: white;} * {
h2 {color: white;} box-sizing: border-box;
}
body {
background-color: #fff;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
font-family: Helvetica, sans-serif;
}
h1 {
color: black;
text-align: center;
margin-bottom: 40px;
}
form {
display: flex;
flex-direction: column;
}
input[type="text"]{
width: 100%;
font-size: 16px;
padding: 8px;
}
input[type="submit"] {
background-color: black;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
width: 100%;
}
input[type="submit"]:hover {
background-color: #333;
}
#error_message {
color: red;
font-weight: bold;
text-align: center;
width: 100%;
margin-top: 20px;
max-width: 300px;
}
</style> </style>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
<h1>WiFi Setup</h1> <h1>01OS Setup</h1>
<form action="/submit" method="post"> <form action="/submit_01os" method="post">
<label for="ssid">SSID:</label><br> <div class="contain">
<input type="text" id="ssid" name="ssid"><br> <label for="server_address">Server Address:</label><br><br>
<label for="password">Password:</label><br> <input type="text" id="server_address" name="server_address"><br><br>
<input type="password" id="password" name="password"><br><br> </div>
<input type="submit" value="Connect">
<input type="submit" value="Connect"/>
<p id="error_message"></p>
</form> </form>
</body> </body>
</html> </html>
)====="; )=====";
const char post_connected_html[] PROGMEM = R"=====( String successHtml = R"=====(
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>01OS Setup</title> <title>01OS Setup</title>
<style> <style>
body {background-color:white;} body {
h1 {color: black;} background-color: #fff;
h2 {color: black;} margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
font-family: Arial, sans-serif;
}
h2 {
color: black;
text-align: center;
margin-bottom: 0px;
</style> </style>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
<h1>01OS Setup</h1> <h2>Connected to 01OS!</h1>
<form action="/submit_01os" method="post"> <p>You can now close this window</p>
<label for="server_address">01OS Server Address:</label><br>
<input type="text" id="server_address" name="server_address"><br>
<input type="submit" value="Connect">
</form>
</body> </body>
</html> </html>
)====="; )=====";
@ -167,28 +248,50 @@ void connectToWifi(String ssid, String password)
bool connectTo01OS(String server_address) bool connectTo01OS(String server_address)
{ {
int err = 0; int err = 0;
int port = 80;
String domain; String domain;
String portStr; String portStr;
if (server_address.indexOf(":") != -1) { // Remove http and https, as it causes errors in HttpClient, the library relies on adding the host header itself
domain = server_address.substring(0, server_address.indexOf(':')); if (server_address.startsWith("http://")) {
portStr = server_address.substring(server_address.indexOf(':') + 1); server_address.remove(0, 7);
} else if (server_address.startsWith("https://")) {
server_address.remove(0, 8);
}
// Remove trailing slash, causes issues
if (server_address.endsWith("/")) {
server_address.remove(server_address.length() - 1);
}
int colonIndex = server_address.indexOf(':');
if (colonIndex != -1) {
domain = server_address.substring(0, colonIndex);
portStr = server_address.substring(colonIndex + 1);
} else { } else {
domain = server_address; domain = server_address;
portStr = ""; // or any default value you want to assign portStr = "";
} }
int port = 0; // Default port value
WiFiClient c;
//If there is a port, set it
if (portStr.length() > 0) { if (portStr.length() > 0) {
port = portStr.toInt(); port = portStr.toInt();
} }
WiFiClient c;
HttpClient http(c, domain.c_str(), port); HttpClient http(c, domain.c_str(), port);
Serial.println("Connecting to 01OS at " + domain + ":" + port + "/ping"); Serial.println("Connecting to 01OS at " + domain + ":" + port + "/ping");
if (domain.indexOf("ngrok") != -1) {
http.sendHeader("ngrok-skip-browser-warning", "80");
}
err = http.get("/ping"); err = http.get("/ping");
// err = http.get("arduino.cc", "/");
bool connectionSuccess = false; bool connectionSuccess = false;
if (err == 0) if (err == 0)
@ -215,7 +318,7 @@ bool connectTo01OS(String server_address)
Serial.print("Content length is: "); Serial.print("Content length is: ");
Serial.println(bodyLen); Serial.println(bodyLen);
Serial.println(); Serial.println();
Serial.println("Body returned follows:"); Serial.println("Body:");
// Now we've got to the body, so we can print it out // Now we've got to the body, so we can print it out
unsigned long timeoutStart = millis(); unsigned long timeoutStart = millis();
@ -229,6 +332,7 @@ bool connectTo01OS(String server_address)
c = http.read(); c = http.read();
// Print out this character // Print out this character
Serial.print(c); Serial.print(c);
Serial.print("");
bodyLen--; bodyLen--;
// We read something, reset the timeout counter // We read something, reset the timeout counter
@ -256,7 +360,7 @@ bool connectTo01OS(String server_address)
} }
else else
{ {
Serial.print("Connect failed: "); Serial.print("Connection failed: ");
Serial.println(err); Serial.println(err);
} }
return connectionSuccess; return connectionSuccess;
@ -304,18 +408,18 @@ void setUpWebserver(AsyncWebServer &server, const IPAddress &localIP)
// Serve Basic HTML Page // Serve Basic HTML Page
server.on("/", HTTP_ANY, [](AsyncWebServerRequest *request) server.on("/", HTTP_ANY, [](AsyncWebServerRequest *request)
{ {
String htmlContent = index_html; String htmlContent = "";
Serial.printf("wifi scan complete: %d . WIFI_SCAN_RUNNING: %d", WiFi.scanComplete(), WIFI_SCAN_RUNNING); Serial.printf("Wifi scan complete: %d . WIFI_SCAN_RUNNING: %d", WiFi.scanComplete(), WIFI_SCAN_RUNNING);
if(WiFi.scanComplete() > 0) { if(WiFi.scanComplete() > 0) {
// Scan complete, process results // Scan complete, process results
Serial.println("done scanning wifi"); Serial.println("Done scanning wifi");
htmlContent = generateHTMLWithSSIDs(); htmlContent = generateHTMLWithSSIDs();
// WiFi.scanNetworks(true); // Start a new scan in async mode // WiFi.scanNetworks(true); // Start a new scan in async mode
} }
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", htmlContent); AsyncWebServerResponse *response = request->beginResponse(200, "text/html", htmlContent);
response->addHeader("Cache-Control", "public,max-age=31536000"); // save this file to cache for 1 year (unless you refresh) response->addHeader("Cache-Control", "public,max-age=31536000"); // save this file to cache for 1 year (unless you refresh)
request->send(response); request->send(response);
Serial.println("Served Basic HTML Page"); }); Serial.println("Served HTML Page"); });
// the catch all // the catch all
server.onNotFound([](AsyncWebServerRequest *request) server.onNotFound([](AsyncWebServerRequest *request)
@ -335,12 +439,19 @@ void setUpWebserver(AsyncWebServer &server, const IPAddress &localIP)
// Check if SSID parameter exists and assign it // Check if SSID parameter exists and assign it
if(request->hasParam("ssid", true)) { if(request->hasParam("ssid", true)) {
ssid = request->getParam("ssid", true)->value(); ssid = request->getParam("ssid", true)->value();
// If "OTHER" is selected, use the value from "otherSSID"
if(ssid == "OTHER" && request->hasParam("otherSSID", true)) {
ssid = request->getParam("otherSSID", true)->value();
Serial.println("OTHER SSID SELECTED: " + ssid);
}
} }
// Check if Password parameter exists and assign it // Check if Password parameter exists and assign it
if(request->hasParam("password", true)) { if(request->hasParam("password", true)) {
password = request->getParam("password", true)->value(); password = request->getParam("password", true)->value();
} }
// Serial.println(ssid);
// Serial.println(password);
// Attempt to connect to the Wi-Fi network with these credentials // Attempt to connect to the Wi-Fi network with these credentials
connectToWifi(ssid, password); connectToWifi(ssid, password);
@ -373,13 +484,27 @@ void setUpWebserver(AsyncWebServer &server, const IPAddress &localIP)
if (connectedToServer) if (connectedToServer)
{ {
connectionMessage = "Connected to 01OS " + server_address; AsyncWebServerResponse *response = request->beginResponse(200, "text/html", successHtml);
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // Prevent caching of this page
request->send(response);
Serial.println(" ");
Serial.println("Connected to 01 websocket!");
Serial.println(" ");
Serial.println("Served success HTML Page");
} }
else else
{ {
connectionMessage = "Couldn't connect to 01OS " + server_address; // If connection fails, serve the error page instead of sending plain text
String htmlContent = String(post_connected_html); // Load your HTML template
// Inject the error message
htmlContent.replace("<p id=\"error_message\"></p>", "<p id=\"error_message\" style=\"color: red;\">Error connecting, please try again.</p>");
AsyncWebServerResponse *response = request->beginResponse(200, "text/html", htmlContent);
response->addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // Prevent caching of this page
request->send(response);
Serial.println("Served Post connection HTML Page with error message");
} }
request->send(200, "text/plain", connectionMessage); }); });
} }
// ----------------------- END OF WIFI CAPTIVE PORTAL ------------------- // ----------------------- END OF WIFI CAPTIVE PORTAL -------------------
@ -444,7 +569,7 @@ void InitI2SSpeakerOrMic(int mode)
#if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(4, 1, 0) #if ESP_IDF_VERSION > ESP_IDF_VERSION_VAL(4, 1, 0)
.communication_format = .communication_format =
I2S_COMM_FORMAT_STAND_I2S, // Set the format of the communication. I2S_COMM_FORMAT_STAND_I2S, // Set the format of the communication.
#else // 设置通讯格式 #else
.communication_format = I2S_COMM_FORMAT_I2S, .communication_format = I2S_COMM_FORMAT_I2S,
#endif #endif
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
@ -551,7 +676,7 @@ void websocket_setup(String server_domain, int port)
return; return;
} }
Serial.println("connected to WiFi"); Serial.println("connected to WiFi");
webSocket.begin(server_domain, port, "/"); webSocket.begin(server_domain, 80, "/");
webSocket.onEvent(webSocketEvent); webSocket.onEvent(webSocketEvent);
// webSocket.setAuthorization("user", "Password"); // webSocket.setAuthorization("user", "Password");
webSocket.setReconnectInterval(5000); webSocket.setReconnectInterval(5000);
@ -559,7 +684,7 @@ void websocket_setup(String server_domain, int port)
void flush_microphone() void flush_microphone()
{ {
Serial.printf("[microphone] flushing %d bytes of data\n", data_offset); Serial.printf("[microphone] flushing and sending %d bytes of data\n", data_offset);
if (data_offset == 0) if (data_offset == 0)
return; return;
webSocket.sendBIN(microphonedata0, data_offset); webSocket.sendBIN(microphonedata0, data_offset);
@ -601,7 +726,7 @@ void setup()
Serial.print("\n"); Serial.print("\n");
M5.begin(true, false, true); M5.begin(true, false, true);
M5.dis.drawpix(0, CRGB(128, 128, 0)); M5.dis.drawpix(0, CRGB(255, 0, 50));
} }
void loop() void loop()
@ -615,18 +740,18 @@ void loop()
if (server_domain != "") if (server_domain != "")
{ {
Serial.println("Setting up websocket to 01OS " + server_domain + ":" + server_port); Serial.println("Setting up websocket to 01OS " + server_domain + ":" + server_port);
websocket_setup(server_domain, server_port); websocket_setup(server_domain, server_port);
InitI2SSpeakerOrMic(MODE_SPK); InitI2SSpeakerOrMic(MODE_SPK);
hasSetupWebsocket = true; hasSetupWebsocket = true;
M5.dis.drawpix(0, CRGB(0, 128, 150));
Serial.println("Websocket connection flow completed"); Serial.println("Websocket connection flow completed");
} }
else // else
{ // {
Serial.println("No valid 01OS server address yet..."); // // Serial.println("No valid 01OS server address yet...");
} // }
// If connected, you might want to do something, like printing the IP address // If connected, you might want to do something, like printing the IP address
// Serial.println("Connected to WiFi!"); // Serial.println("Connected to WiFi!");
// Serial.println("IP Address: " + WiFi.localIP().toString()); // Serial.println("IP Address: " + WiFi.localIP().toString());

@ -5,7 +5,7 @@ import shutil
import time import time
from ..utils.print_markdown import print_markdown from ..utils.print_markdown import print_markdown
def create_tunnel(tunnel_method='bore', server_host='localhost', server_port=8000): def create_tunnel(tunnel_method='ngrok', server_host='localhost', server_port=8000):
print_markdown(f"Exposing server to the internet...") print_markdown(f"Exposing server to the internet...")
if tunnel_method == "bore": if tunnel_method == "bore":
@ -17,9 +17,14 @@ def create_tunnel(tunnel_method='bore', server_host='localhost', server_port=800
exit(1) exit(1)
time.sleep(6) time.sleep(6)
output = subprocess.check_output(f'bore local {server_port} --to bore.pub', shell=True) # output = subprocess.check_output(f'bore local {server_port} --to bore.pub', shell=True)
process = subprocess.Popen(f'bore local {server_port} --to bore.pub', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
for line in output.split('\n'): while True:
line = process.stdout.readline()
print(line)
if not line:
break
if "listening at bore.pub:" in line: if "listening at bore.pub:" in line:
remote_port = re.search('bore.pub:([0-9]*)', line).group(1) remote_port = re.search('bore.pub:([0-9]*)', line).group(1)
print_markdown(f"Your server is being hosted at the following URL: bore.pub:{remote_port}") print_markdown(f"Your server is being hosted at the following URL: bore.pub:{remote_port}")
@ -29,29 +34,62 @@ def create_tunnel(tunnel_method='bore', server_host='localhost', server_port=800
elif tunnel_method == "localtunnel": elif tunnel_method == "localtunnel":
if not subprocess.call('command -v lt', shell=True): if subprocess.call('command -v lt', shell=True):
print("The 'lt' command is not available.") print("The 'lt' command is not available.")
print("Please ensure you have Node.js installed, then run 'npm install -g localtunnel'.") print("Please ensure you have Node.js installed, then run 'npm install -g localtunnel'.")
print("For more information, see https://github.com/localtunnel/localtunnel") print("For more information, see https://github.com/localtunnel/localtunnel")
exit(1) exit(1)
else: else:
output = subprocess.check_output(f'npx localtunnel --port {server_port}', shell=True) process = subprocess.Popen(f'npx localtunnel --port {server_port}', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
for line in output.split('\n'):
if "your url is: https://" in line: found_url = False
remote_url = re.search('https://([a-zA-Z0-9.-]*)', line).group(0).replace('https://', '') url_pattern = re.compile(r'your url is: https://[a-zA-Z0-9.-]+')
print(f"Your server is being hosted at the following URL: {remote_url}")
break while True:
line = process.stdout.readline()
if not line:
break # Break out of the loop if no more output
match = url_pattern.search(line)
if match:
found_url = True
remote_url = match.group(0).replace('your url is: ', '')
print(f"\nYour server is being hosted at the following URL: {remote_url}")
break # Exit the loop once the URL is found
if not found_url:
print("Failed to extract the localtunnel URL. Please check localtunnel's output for details.")
elif tunnel_method == "ngrok": elif tunnel_method == "ngrok":
if not subprocess.call('command -v ngrok', shell=True):
# Check if ngrok is installed
is_installed = subprocess.check_output('command -v ngrok', shell=True).decode().strip()
if not is_installed:
print("The ngrok command is not available.") print("The ngrok command is not available.")
print("Please install ngrok using the instructions at https://ngrok.com/docs/getting-started/") print("Please install ngrok using the instructions at https://ngrok.com/docs/getting-started/")
exit(1) exit(1)
else:
output = subprocess.check_output(f'ngrok http {server_port} --log stdout', shell=True)
for line in output.split('\n'):
if "started tunnel" in line:
remote_url = re.search('https://([a-zA-Z0-9.-]*)', line).group(0).replace('https://', '')
print(f"Your server is being hosted at the following URL: {remote_url}")
break
# If ngrok is installed, start it on the specified port
# process = subprocess.Popen(f'ngrok http {server_port} --log=stdout', shell=True, stdout=subprocess.PIPE)
process = subprocess.Popen(f'ngrok http {server_port} --scheme http,https --log=stdout', shell=True, stdout=subprocess.PIPE)
# Initially, no URL is found
found_url = False
# Regular expression to match the ngrok URL
url_pattern = re.compile(r'https://[a-zA-Z0-9-]+\.ngrok(-free)?\.app')
# Read the output line by line
while True:
line = process.stdout.readline().decode('utf-8')
if not line:
break # Break out of the loop if no more output
match = url_pattern.search(line)
if match:
found_url = True
remote_url = match.group(0)
print(f"\nYour server is being hosted at the following URL: {remote_url}")
break # Exit the loop once the URL is found
if not found_url:
print("Failed to extract the ngrok tunnel URL. Please check ngrok's output for details.")

@ -15,7 +15,7 @@ def run(
server_host: str = typer.Option("0.0.0.0", "--server-host", help="Specify the server host where the server will deploy"), server_host: str = typer.Option("0.0.0.0", "--server-host", help="Specify the server host where the server will deploy"),
server_port: int = typer.Option(8000, "--server-port", help="Specify the server port where the server will deploy"), server_port: int = typer.Option(8000, "--server-port", help="Specify the server port where the server will deploy"),
tunnel_service: str = typer.Option("bore", "--tunnel-service", help="Specify the tunnel service"), tunnel_service: str = typer.Option("ngrok", "--tunnel-service", help="Specify the tunnel service"),
expose: bool = typer.Option(False, "--expose", help="Expose server to internet"), expose: bool = typer.Option(False, "--expose", help="Expose server to internet"),
client: bool = typer.Option(False, "--client", help="Run client"), client: bool = typer.Option(False, "--client", help="Run client"),

Loading…
Cancel
Save