Skip to content

Commit cc2cdeb

Browse files
committed
micropython/net: Add "webrepl" server from main repo.
Signed-off-by: Jim Mussared <[email protected]>
1 parent cf5ed97 commit cc2cdeb

File tree

3 files changed

+286
-0
lines changed

3 files changed

+286
-0
lines changed

micropython/net/webrepl/manifest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module("webrepl.py", opt=3)
2+
module("webrepl_setup.py", opt=3)

micropython/net/webrepl/webrepl.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# This module should be imported from REPL, not run from command line.
2+
import binascii
3+
import hashlib
4+
import network
5+
import os
6+
import socket
7+
import sys
8+
import websocket
9+
import _webrepl
10+
11+
listen_s = None
12+
client_s = None
13+
14+
DEBUG = 0
15+
16+
_DEFAULT_STATIC_HOST = const("https://micropython.org/webrepl/")
17+
static_host = _DEFAULT_STATIC_HOST
18+
19+
20+
def server_handshake(cl):
21+
req = cl.makefile("rwb", 0)
22+
# Skip HTTP GET line.
23+
l = req.readline()
24+
if DEBUG:
25+
sys.stdout.write(repr(l))
26+
27+
webkey = None
28+
upgrade = False
29+
websocket = False
30+
31+
while True:
32+
l = req.readline()
33+
if not l:
34+
# EOF in headers.
35+
return False
36+
if l == b"\r\n":
37+
break
38+
if DEBUG:
39+
sys.stdout.write(l)
40+
h, v = [x.strip() for x in l.split(b":", 1)]
41+
if DEBUG:
42+
print((h, v))
43+
if h == b"Sec-WebSocket-Key":
44+
webkey = v
45+
elif h == b"Connection" and b"Upgrade" in v:
46+
upgrade = True
47+
elif h == b"Upgrade" and v == b"websocket":
48+
websocket = True
49+
50+
if not (upgrade and websocket and webkey):
51+
return False
52+
53+
if DEBUG:
54+
print("Sec-WebSocket-Key:", webkey, len(webkey))
55+
56+
d = hashlib.sha1(webkey)
57+
d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
58+
respkey = d.digest()
59+
respkey = binascii.b2a_base64(respkey)[:-1]
60+
if DEBUG:
61+
print("respkey:", respkey)
62+
63+
cl.send(
64+
b"""\
65+
HTTP/1.1 101 Switching Protocols\r
66+
Upgrade: websocket\r
67+
Connection: Upgrade\r
68+
Sec-WebSocket-Accept: """
69+
)
70+
cl.send(respkey)
71+
cl.send("\r\n\r\n")
72+
73+
return True
74+
75+
76+
def send_html(cl):
77+
cl.send(
78+
b"""\
79+
HTTP/1.0 200 OK\r
80+
\r
81+
<base href=\""""
82+
)
83+
cl.send(static_host)
84+
cl.send(
85+
b"""\"></base>\r
86+
<script src="webrepl_content.js"></script>\r
87+
"""
88+
)
89+
cl.close()
90+
91+
92+
def setup_conn(port, accept_handler):
93+
global listen_s
94+
listen_s = socket.socket()
95+
listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
96+
97+
ai = socket.getaddrinfo("0.0.0.0", port)
98+
addr = ai[0][4]
99+
100+
listen_s.bind(addr)
101+
listen_s.listen(1)
102+
if accept_handler:
103+
listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
104+
for i in (network.AP_IF, network.STA_IF):
105+
iface = network.WLAN(i)
106+
if iface.active():
107+
print("WebREPL server started on http://%s:%d/" % (iface.ifconfig()[0], port))
108+
return listen_s
109+
110+
111+
def accept_conn(listen_sock):
112+
global client_s
113+
cl, remote_addr = listen_sock.accept()
114+
115+
if not server_handshake(cl):
116+
send_html(cl)
117+
return False
118+
119+
prev = os.dupterm(None)
120+
os.dupterm(prev)
121+
if prev:
122+
print("\nConcurrent WebREPL connection from", remote_addr, "rejected")
123+
cl.close()
124+
return False
125+
print("\nWebREPL connection from:", remote_addr)
126+
client_s = cl
127+
128+
ws = websocket.websocket(cl, True)
129+
ws = _webrepl._webrepl(ws)
130+
cl.setblocking(False)
131+
# notify REPL on socket incoming data (ESP32/ESP8266-only)
132+
if hasattr(os, "dupterm_notify"):
133+
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
134+
os.dupterm(ws)
135+
136+
return True
137+
138+
139+
def stop():
140+
global listen_s, client_s
141+
os.dupterm(None)
142+
if client_s:
143+
client_s.close()
144+
if listen_s:
145+
listen_s.close()
146+
147+
148+
def start(port=8266, password=None, accept_handler=accept_conn):
149+
global static_host
150+
stop()
151+
webrepl_pass = password
152+
if webrepl_pass is None:
153+
try:
154+
import webrepl_cfg
155+
156+
webrepl_pass = webrepl_cfg.PASS
157+
if hasattr(webrepl_cfg, "BASE"):
158+
static_host = webrepl_cfg.BASE
159+
except:
160+
print("WebREPL is not configured, run 'import webrepl_setup'")
161+
162+
_webrepl.password(webrepl_pass)
163+
s = setup_conn(port, accept_handler)
164+
165+
if accept_handler is None:
166+
print("Starting webrepl in foreground mode")
167+
# Run accept_conn to serve HTML until we get a websocket connection.
168+
while not accept_conn(s):
169+
pass
170+
elif password is None:
171+
print("Started webrepl in normal mode")
172+
else:
173+
print("Started webrepl in manual override mode")
174+
175+
176+
def start_foreground(port=8266, password=None):
177+
start(port, password, None)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import sys
2+
3+
import os
4+
import machine
5+
6+
RC = "./boot.py"
7+
CONFIG = "./webrepl_cfg.py"
8+
9+
10+
def input_choice(prompt, choices):
11+
while 1:
12+
resp = input(prompt)
13+
if resp in choices:
14+
return resp
15+
16+
17+
def getpass(prompt):
18+
return input(prompt)
19+
20+
21+
def input_pass():
22+
while 1:
23+
passwd1 = getpass("New password (4-9 chars): ")
24+
if len(passwd1) < 4 or len(passwd1) > 9:
25+
print("Invalid password length")
26+
continue
27+
passwd2 = getpass("Confirm password: ")
28+
if passwd1 == passwd2:
29+
return passwd1
30+
print("Passwords do not match")
31+
32+
33+
def exists(fname):
34+
try:
35+
with open(fname):
36+
pass
37+
return True
38+
except OSError:
39+
return False
40+
41+
42+
def get_daemon_status():
43+
with open(RC) as f:
44+
for l in f:
45+
if "webrepl" in l:
46+
if l.startswith("#"):
47+
return False
48+
return True
49+
return None
50+
51+
52+
def change_daemon(action):
53+
LINES = ("import webrepl", "webrepl.start()")
54+
with open(RC) as old_f, open(RC + ".tmp", "w") as new_f:
55+
found = False
56+
for l in old_f:
57+
for patt in LINES:
58+
if patt in l:
59+
found = True
60+
if action and l.startswith("#"):
61+
l = l[1:]
62+
elif not action and not l.startswith("#"):
63+
l = "#" + l
64+
new_f.write(l)
65+
if not found:
66+
new_f.write("import webrepl\nwebrepl.start()\n")
67+
# FatFs rename() is not POSIX compliant, will raise OSError if
68+
# dest file exists.
69+
os.remove(RC)
70+
os.rename(RC + ".tmp", RC)
71+
72+
73+
def main():
74+
status = get_daemon_status()
75+
76+
print("WebREPL daemon auto-start status:", "enabled" if status else "disabled")
77+
print("\nWould you like to (E)nable or (D)isable it running on boot?")
78+
print("(Empty line to quit)")
79+
resp = input("> ").upper()
80+
81+
if resp == "E":
82+
if exists(CONFIG):
83+
resp2 = input_choice(
84+
"Would you like to change WebREPL password? (y/n) ", ("y", "n", "")
85+
)
86+
else:
87+
print("To enable WebREPL, you must set password for it")
88+
resp2 = "y"
89+
90+
if resp2 == "y":
91+
passwd = input_pass()
92+
with open(CONFIG, "w") as f:
93+
f.write("PASS = %r\n" % passwd)
94+
95+
if resp not in ("D", "E") or (resp == "D" and not status) or (resp == "E" and status):
96+
print("No further action required")
97+
sys.exit()
98+
99+
change_daemon(resp == "E")
100+
101+
print("Changes will be activated after reboot")
102+
resp = input_choice("Would you like to reboot now? (y/n) ", ("y", "n", ""))
103+
if resp == "y":
104+
machine.reset()
105+
106+
107+
main()

0 commit comments

Comments
 (0)