import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Filters } from "./Filters"; import { EntriesList } from "./EntriesList"; import { makeStyles } from "@material-ui/core"; import TrafficViewerStyles from "./TrafficViewer.module.sass"; import styles from '../style/EntriesList.module.sass'; import { EntryDetailed } from "./EntryDetailed"; import playIcon from 'assets/run.svg'; import pauseIcon from 'assets/pause.svg'; import variables from '../../variables.module.scss'; import { toast } from 'react-toastify'; import debounce from 'lodash/debounce'; import { RecoilRoot, RecoilState, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import entriesAtom from "../../recoil/entries"; import focusedEntryIdAtom from "../../recoil/focusedEntryId"; import websocketConnectionAtom, { WsConnectionStatus } from "../../recoil/wsConnection"; import queryAtom from "../../recoil/query"; import { TLSWarning } from "../TLSWarning/TLSWarning"; import trafficViewerApiAtom from "../../recoil/TrafficViewerApi" import TrafficViewerApi from "./TrafficViewerApi"; import { StatusBar } from "../UI/StatusBar"; import tappingStatusAtom from "../../recoil/tappingStatus/atom"; const useLayoutStyles = makeStyles(() => ({ details: { flex: "0 0 50%", width: "45vw", padding: "12px 24px", borderRadius: 4, marginTop: 15, background: variables.headerBackgroundColor, }, viewer: { display: "flex", overflowY: "auto", height: "calc(100% - 70px)", padding: 5, paddingBottom: 0, overflow: "auto", }, })); interface TrafficViewerProps { setAnalyzeStatus?: (status: any) => void; api?: any message?: {} error?: {} isWebSocketOpen: boolean trafficViewerApiProp: TrafficViewerApi, actionButtons?: JSX.Element, isShowStatusBar?: boolean } const TrafficViewer: React.FC = ({ setAnalyzeStatus, message, error, isWebSocketOpen, trafficViewerApiProp, actionButtons, isShowStatusBar }) => { const classes = useLayoutStyles(); const [entries, setEntries] = useRecoilState(entriesAtom); const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom); const query = useRecoilValue(queryAtom); const [queryToSend, setQueryToSend] = useState("") const setTrafficViewerApiState = useSetRecoilState(trafficViewerApiAtom as RecoilState) const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom); const [noMoreDataTop, setNoMoreDataTop] = useState(false); const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5"); const [queriedCurrent, setQueriedCurrent] = useState(0); const [queriedTotal, setQueriedTotal] = useState(0); const [leftOffBottom, setLeftOffBottom] = useState(0); const [leftOffTop, setLeftOffTop] = useState(null); const [truncatedTimestamp, setTruncatedTimestamp] = useState(0); const [startTime, setStartTime] = useState(0); const scrollableRef = useRef(null); const [showTLSWarning, setShowTLSWarning] = useState(false); const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false); const [addressesWithTLS, setAddressesWithTLS] = useState(new Set()); const handleQueryChange = useMemo( () => debounce(async (query: string) => { if (!query) { setQueryBackgroundColor("#f5f5f5"); } else { const data = await trafficViewerApiProp.validateQuery(query); if (!data) { return; } if (data.valid) { setQueryBackgroundColor("#d2fad2"); } else { setQueryBackgroundColor("#fad6dc"); } } }, 500), [] ) as (query: string) => void; useEffect(() => { handleQueryChange(query); }, [query, handleQueryChange]); const listEntry = useRef(null); const openWebSocket = (query: string, resetEntries: boolean) => { if (resetEntries) { setFocusedEntryId(null); setEntries([]); setQueriedCurrent(0); setLeftOffTop(null); setNoMoreDataTop(false); } setQueryToSend(query) trafficViewerApiProp.webSocket.open(); } const onmessage = useCallback((e) => { if (!e?.data) return; const message = JSON.parse(e.data); switch (message.messageType) { case "entry": const entry = message.data; if (!focusedEntryId) setFocusedEntryId(entry.id.toString()); const newEntries = [...entries, entry]; if (newEntries.length === 10001) { setLeftOffTop(newEntries[0].entry.id); newEntries.shift(); setNoMoreDataTop(false); } setEntries(newEntries); break; case "status": setTappingStatus(message.tappingStatus); break; case "analyzeStatus": setAnalyzeStatus(message.analyzeStatus); break; case "outboundLink": onTLSDetected(message.Data.DstIP); break; case "toast": toast[message.data.type](message.data.text, { position: "bottom-right", theme: "colored", autoClose: message.data.autoClose, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, }); break; case "queryMetadata": setQueriedCurrent(queriedCurrent + message.data.current); setQueriedTotal(message.data.total); setLeftOffBottom(message.data.leftOff); setTruncatedTimestamp(message.data.truncatedTimestamp); if (leftOffTop === null) { setLeftOffTop(message.data.leftOff - 1); } break; case "startTime": setStartTime(message.data); break; default: console.error( `unsupported websocket message type, Got: ${message.messageType}` ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [message]); useEffect(() => { onmessage(message) }, [message, onmessage]) useEffect(() => { onerror(error) }, [error]) useEffect(() => { isWebSocketOpen ? setWsConnection(WsConnectionStatus.Connected) : setWsConnection(WsConnectionStatus.Closed) trafficViewerApiProp.webSocket.sendQuery(queryToSend) }, [isWebSocketOpen, queryToSend, setWsConnection]) const onerror = (event) => { console.error("WebSocket error:", event); if (query) { openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false); } else { openWebSocket(`leftOff(${leftOffBottom})`, false); } } useEffect(() => { (async () => { setTrafficViewerApiState(trafficViewerApiProp) openWebSocket("leftOff(-1)", true); try { const tapStatusResponse = await trafficViewerApiProp.tapStatus(); setTappingStatus(tapStatusResponse); if (setAnalyzeStatus) { const analyzeStatusResponse = await trafficViewerApiProp.analyzeStatus(); setAnalyzeStatus(analyzeStatusResponse); } } catch (error) { console.error(error); } })() // eslint-disable-next-line }, []); const toggleConnection = () => { if (wsConnection === WsConnectionStatus.Closed) { if (query) { openWebSocket(`(${query}) and leftOff(-1)`, true); } else { openWebSocket(`leftOff(-1)`, true); } scrollableRef.current.jumpToBottom(); setIsSnappedToBottom(true); } else if (wsConnection === WsConnectionStatus.Connected) { trafficViewerApiProp.webSocket.close() setWsConnection(WsConnectionStatus.Closed); } } const onTLSDetected = (destAddress: string) => { addressesWithTLS.add(destAddress); setAddressesWithTLS(new Set(addressesWithTLS)); if (!userDismissedTLSWarning) { setShowTLSWarning(true); } }; const getConnectionIndicator = () => { switch (wsConnection) { case WsConnectionStatus.Connected: return
default: return
} } const getConnectionTitle = () => { switch (wsConnection) { case WsConnectionStatus.Connected: return "streaming live traffic" default: return "streaming paused"; } } const onSnapBrokenEvent = () => { setIsSnappedToBottom(false); if (wsConnection === WsConnectionStatus.Connected) { trafficViewerApiProp.webSocket.close() } } return (
{tappingStatus && isShowStatusBar && }
pause play
{getConnectionTitle()} {getConnectionIndicator()}
{actionButtons}
{
{focusedEntryId && }
}
); }; const MemoiedTrafficViewer = React.memo(TrafficViewer) const TrafficViewerContainer: React.FC = ({ setAnalyzeStatus, message, isWebSocketOpen, trafficViewerApiProp, actionButtons, isShowStatusBar = true }) => { return } export default TrafficViewerContainer