Skip to content

Commit

Permalink
Add websocket packet sniffer page
Browse files Browse the repository at this point in the history
  • Loading branch information
sidoh committed Oct 19, 2024
1 parent df5a64b commit 8fd0a71
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 22 deletions.
2 changes: 1 addition & 1 deletion web2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ I do this only to proxy API requests to an actual ESP8266 running on my local ne
The API client in `web2/api` is generated from the openapi spec using `openapi-zod-client`. Run it with this command:

```bash
openapi-zod-client ../docs/openapi.yaml -o ./api/api-zod.ts --with-description
openapi-zod-client ../docs/openapi.yaml -o ./api/api-zod.ts --with-description --export-schemas
```
27 changes: 27 additions & 0 deletions web2/api/api-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,30 @@ const TransitionData = TransitionArgs.and(
.passthrough()
);
const postTransitions_Body = TransitionData.and(BulbId);
const PacketMessage = z
.object({
t: z.literal("packet").describe("Type of message").optional(),
d: z
.object({
di: z.number().int().describe("Device ID"),
gi: z.number().int().describe("Group ID"),
rt: RemoteType.describe(
"Type of remote to read a packet from. If unspecified, will read packets from all remote types."
),
})
.passthrough()
.describe("The bulb that the packet is for"),
p: z.array(z.number().int()).describe("Raw packet data"),
s: NormalizedGroupState.describe("Group state with a static set of fields"),
u: z
.object({})
.partial()
.passthrough()
.describe("The command represented by the packet"),
})
.passthrough();
const WebSocketMessage = PacketMessage;
const DeviceId = z.array(z.unknown());

export const schemas = {
RemoteType,
Expand Down Expand Up @@ -593,6 +617,9 @@ export const schemas = {
BulbId,
TransitionData,
postTransitions_Body,
PacketMessage,
WebSocketMessage,
DeviceId,
};

const endpoints = makeApi([
Expand Down
16 changes: 16 additions & 0 deletions web2/components/light/light-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import { Skeleton } from "../ui/skeleton";
import { api } from "@/lib/api";
import { z } from "zod";
import { useToast } from "@/hooks/use-toast";
import { useWebSocketContext } from "@/lib/websocket";

export function LightList() {
const { lastMessage } = useWebSocketContext();
const [lightStates, dispatch] = useReducer(reducer, {
lights: [],
isLoading: true,
Expand All @@ -43,6 +45,20 @@ export function LightList() {
loadInitialState();
}, []);

useEffect(() => {
if (lastMessage && lastMessage.t == "packet") {
dispatch({
type: "UPDATE_STATE",
device: {
device_id: lastMessage.d.di,
group_id: lastMessage.d.gi,
device_type: lastMessage.d.rt,
},
payload: lastMessage.s,
});
}
}, [lastMessage]);

const updateGroup = (
light: z.infer<typeof schemas.GatewayListItem>,
state: z.infer<typeof schemas.NormalizedGroupState>
Expand Down
13 changes: 9 additions & 4 deletions web2/components/light/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ export interface LightIndexState {
isLoading: boolean;
}

type Device = Omit<
z.infer<typeof schemas.GatewayListItem>["device"],
"id" | "alias"
>;

type Action =
| {
type: "UPDATE_STATE";
device: z.infer<typeof schemas.GatewayListItem>["device"];
device: Device;
payload: Partial<z.infer<typeof schemas.NormalizedGroupState>>;
}
| {
Expand All @@ -20,7 +25,7 @@ type Action =
}
| {
type: "DELETE_LIGHT";
device: z.infer<typeof schemas.GatewayListItem>["device"];
device: Device;
}
| {
type: "ADD_LIGHT";
Expand All @@ -33,8 +38,8 @@ type Action =
};

function devicesAreEqual(
a: z.infer<typeof schemas.GatewayListItem>["device"],
b: z.infer<typeof schemas.GatewayListItem>["device"]
a: Device,
b: Device
) {
return (
a.device_id === b.device_id &&
Expand Down
3 changes: 2 additions & 1 deletion web2/components/ui/main-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function MainNav({
className="hover:text-slate-900 dark:hover:text-slate-100 text-slate-900 dark:text-slate-100 text-lg font-bold"
href="#/dashboard"
>
Light Hub
MiLight Hub
</Link>
<nav
className={cn(
Expand All @@ -25,6 +25,7 @@ export function MainNav({
{...props}
>
<NavLink href="#/dashboard">Dashboard</NavLink>
<NavLink href="#/sniffer">Sniffer</NavLink>
</nav>
</div>
<Link
Expand Down
1 change: 1 addition & 0 deletions web2/inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const html = `
<link href="data:image/x-icon;base64,AAABAAEAEBAAAAEACABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAA/38NABD+CwD/jQsA9pUOABRu/wD/HQwA+gPrAP4MbAAZ8f8ACxb9AP4GkwCW/AoADYj+AP59CQD7FAkAC//nAJYK/QAQ/w4Acgv/AP71CwCSDP8A+AkYAP7rBAAN/+0A/xEcAP1rCwAM/okADf+HAI/7CgAL/5sADP95AA0U/wD4/gsADXr+ABAO/gDlC/8A+v0NAA3/EwB4Ef8A8wr7AP0IeAAN7P4Abf8LAAuJ/wCVDv8A+hcGACH+DgBz/wsA+g7/ABT+ZQAMCv0Aagn/AJT/DQD+EPsADP7sAOf/DAD/Dx0A/g2AAB4N/gB0/gwADZz/ABLu/wAM/BYA+wqMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkYAAAAAAAAAAAAAAUsAAA+NwAAGzIAAAAAAAAAIj0AKhAAHh8AAAAAAAoAAAANAAAAABwAAAA/AAAzIAAAAAAAAAAAAAAmAgAAADsjAAAAAAAAAAASLwAAAAAAAAAAAAAAAAAAAAAAABMnNAAAAAAAAAAAAAArMDwVLREAAAAAAAAAAAAADDUdAAAAAAAAAAAAAAAAAAAAAAAAJDEAAAAAAAAAACU4AAAAKDYAAAAAAAAAAAAAFCEAAAcAAAA6AAAAAAEAAAAXAAAAAABACAAWLgAaAwAAAAAAAAALKQAAOQYAAA4EAAAAAAAAAAAAABkPAAAAAAAAAP5/AADmZwAA8k8AALvdAACf+QAAz/MAAP//AAAf+AAAH/gAAP//AADP8wAAn/kAALvdAADyTwAA5mcAAP5/AAA=" rel="icon" type="image/x-icon" />
</head>
<body>
<div id="page"></div>
</body>
${isProduction
? '<script src="bundle.js"></script>'
Expand Down
57 changes: 57 additions & 0 deletions web2/lib/websocket.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { schemas } from "@/api";
import React, {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
import useWebSocket from "react-use-websocket";
import { z } from "zod";

type WebSocketMessage = z.infer<typeof schemas.WebSocketMessage>;

interface WebSocketContextType {
lastMessage: WebSocketMessage;
allMessages: WebSocketMessage[];
}

const WebSocketContext = createContext<WebSocketContextType | null>(null);

export const WebSocketProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const { lastJsonMessage, sendJsonMessage } =
useWebSocket(`ws://10.133.8.88:81`);
const [messages, setMessages] = useState<WebSocketMessage[]>([]);

useEffect(() => {
if (lastJsonMessage !== null) {
setMessages((messages) => [
...messages,
lastJsonMessage as WebSocketMessage,
]);
}
}, [lastJsonMessage]);

return (
<WebSocketContext.Provider
value={{
lastMessage: messages[messages.length - 1],
allMessages: messages,
}}
>
{children}
</WebSocketContext.Provider>
);
};

export const useWebSocketContext = () => {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error(
"useWebSocketContext must be used within a WebSocketProvider"
);
}
return context;
};
7 changes: 7 additions & 0 deletions web2/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"react-router": "^6.26.2",
"react-router-dom": "^6.26.2",
"react-select": "^5.8.1",
"react-use-websocket": "^4.9.0",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-radix": "^3.0.5",
Expand Down
24 changes: 19 additions & 5 deletions web2/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@ const path = require('path');
const app = express();
const PORT = 3000; // You can change this to any port you prefer
const PROXY_URL = process.env.PROXY_URL || 'http://10.133.8.88'; // Replace with your proxy URL
const WS_PROXY_URL = process.env.WS_PROXY_URL || `ws://10.133.8.88:81`; // WebSocket proxy URL, defaults to PROXY_URL if not set

const proxyMiddleware = createProxyMiddleware({
// HTTP proxy middleware
const httpProxyMiddleware = createProxyMiddleware({
target: PROXY_URL,
changeOrigin: true,
});

// WebSocket proxy middleware
const wsProxyMiddleware = createProxyMiddleware({
target: WS_PROXY_URL,
changeOrigin: true,
ws: true, // Enable WebSocket proxying
});

// Serve static files from the 'dist' directory
app.use(express.static(path.join(__dirname, 'dist')));

// Proxy requests that aren't files
app.use('/', proxyMiddleware);
// Proxy HTTP requests that aren't files
app.use('/', httpProxyMiddleware);

app.listen(PORT, () => {
const server = app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
console.log(`HTTP requests proxied to: ${PROXY_URL}`);
console.log(`WebSocket connections proxied to: ${WS_PROXY_URL}`);
});

// Upgrade HTTP server to also handle WebSocket connections
server.on('upgrade', wsProxyMiddleware.upgrade);
32 changes: 21 additions & 11 deletions web2/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { Dashboard } from './pages/dashboard';
import { NotFound } from './pages/not-found';
import SettingsPage from './pages/settings/settings-index';
import { Toaster } from '@/components/ui/toaster';
import { WebSocketProvider } from '@/lib/websocket';
import SnifferPage from './pages/sniffer';

const PAGES = {
"/dashboard": Dashboard,
"/not-found": NotFound,
"/settings": SettingsPage
"/settings": SettingsPage,
"/sniffer": SnifferPage
}

export default function App() {
Expand All @@ -36,17 +39,24 @@ export default function App() {
const PageComponent = currentPage ? PAGES[currentPage] || PAGES["/not-found"] : null;

return (
<div className="bg-background text-foreground flex flex-col items-center justify-start">
<div className="container mx-auto px-4">
<MainNav />
<main className="flex flex-col pt-10">
{PageComponent && <PageComponent />}
</main>
<Toaster />
<WebSocketProvider>
<div className="bg-background text-foreground flex flex-col items-center justify-start">
<div className="container mx-auto px-4">
<MainNav />
<main className="flex flex-col pt-10">
{PageComponent && <PageComponent />}
</main>
<Toaster />
</div>
</div>
</div>
</WebSocketProvider>
);
}

const root = createRoot(document.body);
root.render(<App />);
const rootElement = document.getElementById('page');
if (rootElement) {
const root = createRoot(rootElement);
root.render(<App />);
} else {
console.error("Could not find element with id 'page'");
}
Loading

0 comments on commit 8fd0a71

Please sign in to comment.