44import libinspector .global_state
55import common
66import logging
7+ import functools
8+ import threading
9+ import os
10+ import requests
11+
712
813logger = logging .getLogger ("client" )
914
@@ -14,6 +19,112 @@ def show():
1419 toast_obj = st .toast ('Discovering devices...' )
1520 show_list (toast_obj )
1621
22+ @functools .lru_cache (maxsize = 1 )
23+ def start_inspector_once ():
24+ """Initialize the Inspector core only once."""
25+ with st .spinner ("Starting API Thread..." ):
26+ threading .Thread (
27+ target = worker_thread ,
28+ daemon = True ,
29+ )
30+
31+
32+ def worker_thread ():
33+ """
34+ A worker thread to periodically clear the cache of call_predict_api.
35+ """
36+ while True :
37+ time .sleep (15 )
38+ db_conn , rwlock = libinspector .global_state .db_conn_and_lock
39+ # Get the list of devices from the database
40+ sql = """
41+ SELECT * \
42+ FROM devices
43+ WHERE is_gateway = 0 \
44+ """
45+ device_list = []
46+ with rwlock :
47+ for device_dict in db_conn .execute (sql ):
48+ device_list .append (dict (device_dict ))
49+
50+ # Getting inputs and calling API
51+ for device_dict in device_list :
52+ dhcp_hostname , oui_vendor = common .get_device_metadata (device_dict ['mac_address' ])
53+ remote_hostnames = common .get_remote_hostnames (device_dict ['mac_address' ])
54+ try :
55+ api_output = call_predict_api (dhcp_hostname , oui_vendor , remote_hostnames , device_dict ['mac_address' ])
56+ common .config_set (f'device_details@{ device_dict ["mac_address" ]} ' , api_output )
57+ except RuntimeError :
58+ continue
59+
60+
61+ @functools .cache
62+ def call_predict_api (dhcp_hostname : str , oui_vendor : str , remote_hostnames : str ,
63+ mac_address : str , url = "https://dev-id-1.tailcedbd.ts.net/predict" ) -> dict :
64+ """
65+ Call the predicting API with the given fields.
66+ This takes the MAC Address of an inspected device
67+ and checks the `devices` table, where iot inspector core collected meta-data
68+ based on SSDP discovery.
69+ Please see Page 11 Table A.1. We explain how to get the data from IoT Inspector:
70+ 1. oui_friendly: we use the OUI database from IoT Inspector Core
71+ 2. dhcp_hostname: this is extracted from the 'devices' table, check meta-data and look for 'dhcp_hostname' key.
72+ 3. remote_hostnames: IoT Inspector collects this information the DHCP hostname via either DNS or SNI
73+ Args:
74+ dhcp_hostname (str): The DHCP hostname of the device we want to use AI to get more info about
75+ oui_vendor (str): The OUI vendor of the device we want to use AI to get more info about
76+ remote_hostnames (str): The remote hostnames the device has contacted
77+ mac_address (str): The MAC address of the device we want to use AI to get more info about
78+ url (str): The API endpoint.
79+ Returns:
80+ dict: The response text from the API.
81+ """
82+ api_key = os .environ .get ("API_KEY" , "momo" )
83+ device_tracked_key = f'tracked@{ mac_address } '
84+
85+ headers = {
86+ "Content-Type" : "application/json" ,
87+ "x-api-key" : api_key
88+ }
89+ data = {
90+ "prolific_id" : common .config_get ("prolific_id" , "" ),
91+ "mac_address" : mac_address ,
92+ "fields" : {
93+ "oui_friendly" : oui_vendor ,
94+ "dhcp_hostname" : dhcp_hostname ,
95+ "remote_hostnames" : remote_hostnames ,
96+ "user_agent_info" : "" ,
97+ "netdisco_info" : "" ,
98+ "user_labels" : "" ,
99+ "talks_to_ads" : common .config_get (device_tracked_key , False )
100+ }
101+ }
102+ non_empty_field_values = [
103+ field_value
104+ for field_name , field_value in data ["fields" ].items ()
105+ if field_name != "talks_to_ads" and bool (field_value )
106+ ]
107+ # TODO: We should make this 2 fields eventually...
108+ if len (non_empty_field_values ) < 1 :
109+ logger .warning (
110+ "[Device ID API] Fewer than two string fields in data are non-empty; refusing to call API. Wait until IoT Inspector collects more data." )
111+ raise RuntimeError (
112+ "Fewer than two string fields in data are non-empty; refusing to call API. Wait until IoT Inspector collects more data." )
113+
114+ # TODO: Used for debugging, risk is minimal since this is client-facing code. Should have some flag for production.
115+ logger .info ("[Device ID API] Calling API with data: %s" , json .dumps (data , indent = 4 ))
116+
117+ try :
118+ response = requests .post (url , headers = headers , json = data , timeout = 10 )
119+ response .raise_for_status ()
120+ result = response .json ()
121+ except (requests .exceptions .RequestException , ValueError ) as e :
122+ logger .error (f"[Device ID API] API request failed: { e } " )
123+ raise RuntimeError ("API request failed, not caching this result." )
124+
125+ logger .info ("[Device ID API] API query successful!" )
126+ return result
127+
17128
18129@st .fragment (run_every = 1 )
19130def show_list (toast_obj : st .toast ):
@@ -58,7 +169,6 @@ def show_device_card(device_dict: dict):
58169 Args:
59170 device_dict (dict): information about the device, from the 'devices' table
60171 """
61-
62172 # Check if the user has previously inspected this device
63173 device_inspected_config_key = f'device_is_inspected_{ device_dict ["mac_address" ]} '
64174 is_inspected = common .config_get (device_inspected_config_key , False )
@@ -90,18 +200,13 @@ def show_device_card(device_dict: dict):
90200 caption = f'{ device_dict ["ip_address" ]} | { device_dict ["mac_address" ]} '
91201 if "oui_vendor" in metadata_dict :
92202 caption += f' | { metadata_dict ["oui_vendor" ]} '
93- try :
94- dhcp_hostname , oui_vendor = common .get_device_metadata (device_dict ['mac_address' ])
95- remote_hostnames = common .get_remote_hostnames (device_dict ['mac_address' ])
96- api_output = common .call_predict_api (dhcp_hostname , oui_vendor , remote_hostnames , device_dict ['mac_address' ])
203+
204+ api_output = common .config_get (f'device_details@{ device_dict ["mac_address" ]} ' , default = {})
205+ if "Vendor" in api_output or "Explanation" in api_output :
97206 vendor = api_output .get ("Vendor" , "" )
98207 explanation = api_output .get ("Explanation" , "" )
99208 if vendor or explanation :
100209 caption += f"| Vendor: { vendor } | Explanation: { explanation } "
101- # Use run-time error to avoid caching if API call failed
102- except RuntimeError as e :
103- logger .info (f"Device ID API failed: { e } " )
104-
105210 st .caption (caption , help = 'IP address, MAC address, manufacturer OUI, and Device Identification API output' )
106211
107212 # --- Add bar charts for upload/download ---
0 commit comments