mirror of
https://github.com/logicog/RTLPlayground.git
synced 2026-11-01 18:04:12 +08:00
Porting to python so it's easier to implement a full
switch-emulator with state
This commit is contained in:
1 parent
a18c334139
commit
a03b2259ef
1 file changed
+504
Executable
+504
@@ -0,0 +1,504 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Serve files from the current directory and provide the example device API."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from email.parser import BytesParser
|
||||||
|
from email.policy import default
|
||||||
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import parse_qs, unquote, urlsplit
|
||||||
|
|
||||||
|
HOST = "0.0.0.0"
|
||||||
|
PORT = 8081
|
||||||
|
PASSWORD = "1234"
|
||||||
|
|
||||||
|
PORTS = 6
|
||||||
|
NSFP = 1
|
||||||
|
N_COUNTERS = 52
|
||||||
|
VLAN_MAX_ID = 4094
|
||||||
|
L2_MAX_TRANSFER = 5
|
||||||
|
|
||||||
|
ROOT = Path.cwd().resolve()
|
||||||
|
|
||||||
|
phys_to_log_port = []
|
||||||
|
|
||||||
|
if PORTS == 9:
|
||||||
|
phys_to_log_port = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
|
||||||
|
else:
|
||||||
|
phys_to_log_port = [ 4, 5, 6, 7, 3, 8 ]
|
||||||
|
|
||||||
|
l2_entries = [
|
||||||
|
{ "mac": "00:01:2f:00:00:01", "vlan":"001", "type":'l', "port":7, "idx":0x10 },
|
||||||
|
{ "mac": "00:06:78:00:00:02", "vlan":"001", "type":'l', "port":7, "idx":0x20 },
|
||||||
|
{ "mac": "00:01:2e:00:00:03", "vlan":"001", "type":'s', "port":7, "idx":0x30 },
|
||||||
|
{ "mac": "60:7d:09:00:00:04", "vlan":"001", "type":'l', "port":7, "idx":0x40 },
|
||||||
|
{ "mac": "1c:2a:a3:00:00:05", "vlan":"001", "type":'l', "port":7, "idx":0x50 },
|
||||||
|
{ "mac": "1c:2a:a3:00:00:06", "vlan":"001", "type":'s', "port":7, "idx":0x60 },
|
||||||
|
{ "mac": "02:e0:4c:00:00:07", "vlan":"001", "type":'l', "port":7, "idx":0x70 },
|
||||||
|
{ "mac": "00:e0:4c:00:00:08", "vlan":"001", "type":'l', "port":4, "idx":0x80 },
|
||||||
|
{ "mac": "1c:2a:a3:00:00:09", "vlan":"001", "type":'l', "port":9, "idx":0x90 }, # CPU-Port
|
||||||
|
{ "mac": "3c:8c:f8:00:00:0a", "vlan":"001", "type":'l', "port":7, "idx":0xa0 },
|
||||||
|
{ "mac": "00:01:2e:00:00:0b", "vlan":"001", "type":'l', "port":7, "idx":0xb0 },
|
||||||
|
{ "mac": "00:01:2f:00:01:01", "vlan":"001", "type":'l', "port":7, "idx":0xc0 }
|
||||||
|
]
|
||||||
|
|
||||||
|
state = {
|
||||||
|
"lldp_enabled": False,
|
||||||
|
"vlans": {1: {"active": True, "name": ""}}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_history = []
|
||||||
|
tx_g = [0] * PORTS
|
||||||
|
rx_g = [0] * PORTS
|
||||||
|
tx_b = [0] * PORTS
|
||||||
|
rx_b = [0] * PORTS
|
||||||
|
last_called = int(time.time())
|
||||||
|
|
||||||
|
sessions = set()
|
||||||
|
uploaded_config = None
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=str(ROOT), **kwargs)
|
||||||
|
|
||||||
|
def api_handlers(self,query):
|
||||||
|
return {
|
||||||
|
"/status.json": self._status,
|
||||||
|
"/eee.json": self._eee,
|
||||||
|
"/bandwidth.json": self._bandwidth,
|
||||||
|
"/information.json": self._information,
|
||||||
|
"/mirror.json": self._mirror,
|
||||||
|
"/lag.json": self._lag,
|
||||||
|
"/l2.json": lambda: self._l2(query),
|
||||||
|
"/mtu.json": self._mtu,
|
||||||
|
"/vlan.json": lambda: self._vlan(query),
|
||||||
|
"/vlanlist": self._vlanlist,
|
||||||
|
"/lldp.json": self._lldp,
|
||||||
|
"/cmd_log": self._cmd_log,
|
||||||
|
"/counters.json": self._counters,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _send(self, status, body=b"", content_type="text/plain",
|
||||||
|
headers=()):
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
for name, value in headers:
|
||||||
|
self.send_header(name, value)
|
||||||
|
self.end_headers()
|
||||||
|
if body:
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def _json(self, value, headers=()):
|
||||||
|
import json
|
||||||
|
|
||||||
|
body = json.dumps(value, separators=(",", ":")).encode("utf-8")
|
||||||
|
return self._send(
|
||||||
|
200, body, "application/json", headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
def _redirect(self, location, headers=()):
|
||||||
|
return self._send(
|
||||||
|
302, headers=(("Location", location), *headers)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _authenticated(self):
|
||||||
|
for item in self.headers.get("Cookie", "").split(";"):
|
||||||
|
name, _, value = item.strip().partition("=")
|
||||||
|
if name == "session" and value in sessions:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _protected(self):
|
||||||
|
if not self._authenticated():
|
||||||
|
self._send(401, b"Unauthorized\n")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _int_param(query, name):
|
||||||
|
try:
|
||||||
|
return int(query.get(name, ["0"])[0], 0)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _api_response(self, path, query):
|
||||||
|
handler = self.api_handlers(query).get(path)
|
||||||
|
return handler() if handler else {}
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
global uploaded_config
|
||||||
|
|
||||||
|
parsed = urlsplit(self.path)
|
||||||
|
path = unquote(parsed.path)
|
||||||
|
query = parse_qs(parsed.query)
|
||||||
|
|
||||||
|
if path in self.api_handlers(None).keys():
|
||||||
|
if self._protected():
|
||||||
|
return
|
||||||
|
result = self._api_response(path, query)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
body, content_type = result
|
||||||
|
return self._send(200, body, content_type)
|
||||||
|
return self._json(
|
||||||
|
result,
|
||||||
|
headers=(("Cache-Control", "no-cache"),)
|
||||||
|
if path in ("/status.json", "/counters.json")
|
||||||
|
else (),
|
||||||
|
)
|
||||||
|
|
||||||
|
if path == "/config" and uploaded_config is not None:
|
||||||
|
if self._protected():
|
||||||
|
return
|
||||||
|
return self._send(200, uploaded_config, "text/plain")
|
||||||
|
|
||||||
|
if not self._authenticated() and path not in ("/login.html", "/favicon.ico"):
|
||||||
|
return self._redirect("/login.html")
|
||||||
|
|
||||||
|
# Resolve files under ROOT only; reject path traversal.
|
||||||
|
relative = path.lstrip("/") or "index.html"
|
||||||
|
target = (ROOT / relative).resolve()
|
||||||
|
if target != ROOT and ROOT not in target.parents:
|
||||||
|
return self._send(404, b"Not found\n")
|
||||||
|
|
||||||
|
if not target.is_file():
|
||||||
|
return self._send(404, b"Not found\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = target.read_bytes()
|
||||||
|
except OSError:
|
||||||
|
return self._send(404, b"Not found\n")
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
mime = mimetypes.guess_type(str(target))[0] or "application/octet-stream"
|
||||||
|
return self._send(
|
||||||
|
200,
|
||||||
|
data,
|
||||||
|
f"{mime}",
|
||||||
|
headers=(("Cache-Control", "max-age=60, must-revalidate"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
global uploaded_config
|
||||||
|
|
||||||
|
path = urlsplit(self.path).path
|
||||||
|
|
||||||
|
if path == "/login":
|
||||||
|
form = self._read_urlencoded_form()
|
||||||
|
if form.get("pwd", [""])[0] == PASSWORD:
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
sessions.add(token)
|
||||||
|
return self._redirect(
|
||||||
|
"/index.html",
|
||||||
|
headers=((
|
||||||
|
"Set-Cookie",
|
||||||
|
f"session={token}; Path=/; SameSite=Strict; HttpOnly",
|
||||||
|
),),
|
||||||
|
)
|
||||||
|
return self._redirect("/login.html")
|
||||||
|
|
||||||
|
if self._protected():
|
||||||
|
return
|
||||||
|
|
||||||
|
if path == "/cmd":
|
||||||
|
form = self._read_urlencoded_form()
|
||||||
|
command = form.get("cmd", [""])[0]
|
||||||
|
cmd_history.append(command)
|
||||||
|
|
||||||
|
self.parse_cmd(command);
|
||||||
|
|
||||||
|
return self._send(
|
||||||
|
200,
|
||||||
|
b"<!DOCTYPE html> <html><head><title>Upload OK</title></head>"
|
||||||
|
b"<body><h1>Command executed successully</h1></html>",
|
||||||
|
"text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
if path in ("/upload", "/config"):
|
||||||
|
if self.headers.get_content_type() != "multipart/form-data":
|
||||||
|
return self._send(400, b"Expected multipart/form-data\n")
|
||||||
|
|
||||||
|
parts = self._read_multipart()
|
||||||
|
if parts is None:
|
||||||
|
return self._send(400, b"Bad multipart request\n")
|
||||||
|
|
||||||
|
file_data = next(
|
||||||
|
(
|
||||||
|
data for content_type, data in parts
|
||||||
|
if content_type == "application/octet-stream"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if file_data is None:
|
||||||
|
return self._send(
|
||||||
|
400, b"No application/octet-stream part\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if path == "/config":
|
||||||
|
uploaded_config = file_data
|
||||||
|
|
||||||
|
return self._send(
|
||||||
|
200,
|
||||||
|
b"<!DOCTYPE html> <html><head><title>Upload OK</title></head>"
|
||||||
|
b"<h1>File uploaded successfully</h1></body></html>",
|
||||||
|
"text/html",
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._send(404, b"Not found\n")
|
||||||
|
|
||||||
|
def _read_urlencoded_form(self):
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
raw = self.rfile.read(length).decode("utf-8", errors="replace")
|
||||||
|
return parse_qs(raw)
|
||||||
|
|
||||||
|
def _read_multipart(self):
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
raw = self.rfile.read(length)
|
||||||
|
content_type = self.headers.get("Content-Type", "")
|
||||||
|
message = BytesParser(policy=default).parsebytes(
|
||||||
|
b"Content-Type: "
|
||||||
|
+ content_type.encode("utf-8")
|
||||||
|
+ b"\r\nMIME-Version: 1.0\r\n\r\n"
|
||||||
|
+ raw
|
||||||
|
)
|
||||||
|
|
||||||
|
if not message.is_multipart():
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for part in message.iter_parts():
|
||||||
|
parts.append((
|
||||||
|
part.get_content_type(),
|
||||||
|
part.get_payload(decode=True) or b"",
|
||||||
|
))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
def parse_cmd(self, cmd):
|
||||||
|
pieces = cmd.split(" ")
|
||||||
|
handler = self.get_parsers().get(pieces[0])
|
||||||
|
return handler(pieces[1:]) if handler else {}
|
||||||
|
|
||||||
|
def get_parsers(self):
|
||||||
|
return {
|
||||||
|
"vlan": self._parse_vlan,
|
||||||
|
"lldp": self._parse_lldp
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_vlan(self, pieces): #vlan 2 name 6 3 8t or vlan 2 6 3 8t or vlan 2 d
|
||||||
|
global state
|
||||||
|
vid = int(pieces[1]);
|
||||||
|
startIndex=2
|
||||||
|
if pieces[2] == "d":
|
||||||
|
return remove_vlan(vid)
|
||||||
|
elif re.match(pieces[2], '^[A-Za-z]+$'):
|
||||||
|
name = pieces[2]
|
||||||
|
startIndex=3
|
||||||
|
|
||||||
|
state.vlans[vid] = {"active":True, "name":name, "untagged":[], "tagged":[]}
|
||||||
|
# list of ports in either 3 or 5t form
|
||||||
|
for i in pieces[startIndex:]:
|
||||||
|
if pieces[i][-1] == "t":
|
||||||
|
state.vlans[vid].tagged.append(pieces[i])
|
||||||
|
else:
|
||||||
|
state.vlans[vid].untagged.append(pieces[i])
|
||||||
|
|
||||||
|
def _parse_lldp(self,pieces):
|
||||||
|
global state
|
||||||
|
state.lldp_enabled = (pieces[0] == "on")
|
||||||
|
|
||||||
|
def _status(self):
|
||||||
|
global last_called
|
||||||
|
now = max(int(time.time()), last_called + 1)
|
||||||
|
elapsed = now - last_called
|
||||||
|
ports = []
|
||||||
|
|
||||||
|
for i in range(1, PORTS + 1):
|
||||||
|
n = i - 1
|
||||||
|
if i % 2:
|
||||||
|
rate = 2_400_000_000 if i == 1 else 950_000_000
|
||||||
|
tx_g[n] += rate * elapsed
|
||||||
|
rx_g[n] += rate * elapsed
|
||||||
|
tx_b[n] += rate * elapsed // 10_000_000
|
||||||
|
rx_b[n] += rate * elapsed // 10_000_000
|
||||||
|
|
||||||
|
item = {
|
||||||
|
"portNum": i,
|
||||||
|
"logPort": phys_to_log_port[n],
|
||||||
|
"isSFP": int(i > PORTS - NSFP),
|
||||||
|
"enabled": int(i % 4 != 0),
|
||||||
|
"link": (5 if i == 1 else 2) if i % 2 else 0,
|
||||||
|
"txG": f"0x{tx_g[n]:016x}",
|
||||||
|
"txB": f"0x{tx_b[n]:016x}",
|
||||||
|
"rxG": f"0x{rx_g[n]:016x}",
|
||||||
|
"rxB": f"0x{rx_b[n]:016x}",
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= PORTS - NSFP:
|
||||||
|
item.update({
|
||||||
|
"sfp_vendor": "OEM",
|
||||||
|
"sfp_model": "10G-SFP+",
|
||||||
|
"sfp_serial": "12345678",
|
||||||
|
"sfp_options": "0x68",
|
||||||
|
"sfp_temp": f"0x{0x28fb + secrets.randbelow(100):04x}",
|
||||||
|
"sfp_vcc": f"0x{0x7eda + secrets.randbelow(100):04x}",
|
||||||
|
"sfp_txbias": f"0x{0x0d24 + secrets.randbelow(100):04x}",
|
||||||
|
"sfp_txpower": f"0x{0x14bd + secrets.randbelow(100):04x}",
|
||||||
|
"sfp_rxpower": "0x0000",
|
||||||
|
"sfp_laser": "0x0000",
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
item["adv"] = (
|
||||||
|
"100000" if i == 1
|
||||||
|
else "000011" if i == 2
|
||||||
|
else "000100"
|
||||||
|
)
|
||||||
|
|
||||||
|
ports.append(item)
|
||||||
|
|
||||||
|
last_called = now
|
||||||
|
return ports
|
||||||
|
|
||||||
|
def _eee(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"portNum": i,
|
||||||
|
"isSFP": int(i >= 5),
|
||||||
|
"eee": "00000010",
|
||||||
|
"eee_lp": "00000100",
|
||||||
|
"active": int(i % 2 == 1),
|
||||||
|
}
|
||||||
|
for i in range(1, PORTS + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _bandwidth(self):
|
||||||
|
ports = []
|
||||||
|
for i in range(1, PORTS + 1):
|
||||||
|
ingress_limited = i % 2 == 1
|
||||||
|
egress_limited = i % 4 != 0
|
||||||
|
ports.append({
|
||||||
|
"portNum": i,
|
||||||
|
"iLimited": int(ingress_limited),
|
||||||
|
"iFC": int(i % 4 != 0) if ingress_limited else 0,
|
||||||
|
"iBW": f"{0x100 if ingress_limited else 0xfffff:08x}",
|
||||||
|
"eLimited": int(egress_limited),
|
||||||
|
"eBW": f"{0x1000 if egress_limited else 0xfffff:08x}",
|
||||||
|
})
|
||||||
|
return ports
|
||||||
|
|
||||||
|
def _mirror(self):
|
||||||
|
return {
|
||||||
|
"mPort": 1,
|
||||||
|
"enabled": 1,
|
||||||
|
"mirror_tx": f"{0b000110:016b}",
|
||||||
|
"mirror_rx": f"{0b000010:016b}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _lag(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"lagNum": i,
|
||||||
|
"members": f"{(0x30 << (i * 2)) & 0x1f8:016b}",
|
||||||
|
"hash": "0x7e",
|
||||||
|
}
|
||||||
|
for i in range(4)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _mtu(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"portNum": i,
|
||||||
|
"mtu": f"0x{16383 if i % 2 else 1522:04x}",
|
||||||
|
}
|
||||||
|
for i in range(1, PORTS + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _lldp(self):
|
||||||
|
return {
|
||||||
|
"on": 1,
|
||||||
|
"portStatus": [
|
||||||
|
{
|
||||||
|
"portNum": i,
|
||||||
|
"logPort": i - 1,
|
||||||
|
"name": "",
|
||||||
|
"permitted": 1,
|
||||||
|
}
|
||||||
|
for i in range(1, PORTS + 1)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _counters(self):
|
||||||
|
return [f"0x{0x1234 + i:016x}" for i in range(N_COUNTERS)]
|
||||||
|
|
||||||
|
def _vlanlist(self):
|
||||||
|
global state
|
||||||
|
vlan_list = []
|
||||||
|
for i in state.get("vlans").keys():
|
||||||
|
if i in state.get("vlans") and state.get("vlans")[i]["active"]:
|
||||||
|
vlan_list.append({
|
||||||
|
"id": i,
|
||||||
|
"name": state.get("vlans")[i]["name"],
|
||||||
|
})
|
||||||
|
return {"mgmt": 1, "vlan": vlan_list}
|
||||||
|
|
||||||
|
def _vlan(self,query):
|
||||||
|
global state
|
||||||
|
vlan_id = self._int_param(query, "vid")
|
||||||
|
if vlan_id == 1:
|
||||||
|
return {
|
||||||
|
"members": "0x0207ffff",
|
||||||
|
"name": "native",
|
||||||
|
"pvid": "0x01ff",
|
||||||
|
}
|
||||||
|
|
||||||
|
vlan = state.get("vlans").get(vlan_id)
|
||||||
|
if vlan and vlan.get("active"):
|
||||||
|
return {
|
||||||
|
"name": vlan["name"],
|
||||||
|
"members": "0x0000",
|
||||||
|
"pvid": "0x0000",
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _l2(self,query):
|
||||||
|
idx = self._int_param(query, "idx")
|
||||||
|
entries = sorted(l2_entries, key=lambda entry: entry["idx"])
|
||||||
|
start = next(
|
||||||
|
(n for n, entry in enumerate(entries) if entry["idx"] >= idx),
|
||||||
|
len(entries),
|
||||||
|
)
|
||||||
|
result = []
|
||||||
|
for entry in entries[start:start + L2_MAX_TRANSFER]:
|
||||||
|
result.append({
|
||||||
|
"mac": entry["mac"],
|
||||||
|
"vlan": entry["vlan"],
|
||||||
|
"type": "l" if entry.get("type") == "l" else "s",
|
||||||
|
"port": entry["port"],
|
||||||
|
"idx": f"{entry['idx']:04x}",
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _cmd_log(self):
|
||||||
|
text = "\n".join(cmd_history)
|
||||||
|
if cmd_history:
|
||||||
|
text += "\n"
|
||||||
|
return text.encode("utf-8"), "text/plain"
|
||||||
|
|
||||||
|
def _information(self):
|
||||||
|
return {
|
||||||
|
"ip_address":"192.168.10.247",
|
||||||
|
"ip_gateway":"192.168.2.22",
|
||||||
|
"ip_netmask":"255.255.255.0",
|
||||||
|
"mac_address":"1c:2a:a3:23:00:02",
|
||||||
|
"sw_ver":"0.1-sim",
|
||||||
|
"hw_ver":"SWGT024-V2.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Serving {ROOT} on http://{HOST}:{PORT}")
|
||||||
|
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
||||||
Reference in new issue
Block a user