1616 BodyPartReader ,
1717 ClientResponse ,
1818 ClientSession ,
19+ Fingerprint ,
1920 MultipartReader ,
20- TCPConnector ,
2121 hdrs ,
2222)
2323from PIL import Image
@@ -123,40 +123,51 @@ def __init__(
123123 * ,
124124 token : str | None = None ,
125125 request_timeout : int = 10 ,
126+ session : ClientSession | None = None ,
126127 verify_ssl : bool = True ,
127128 ssl_ca_cert : str | None = None ,
129+ ssl_fingerprint : str | None = None ,
128130 use_password_obfuscation : bool | None = None ,
129131 ) -> None :
130132 """
131133 Initialize the NanoKVM client.
132134
133135 Args:
134136 url: Base URL of the NanoKVM API (e.g., "https://kvm.local/api/")
137+ session: aiohttp ClientSession to use for requests.
135138 token: Optional pre-existing authentication token
136139 request_timeout: Request timeout in seconds (default: 10)
137140 verify_ssl: Enable SSL certificate verification (default: True).
138141 Set to False to disable verification for self-signed certificates.
139142 ssl_ca_cert: Path to custom CA certificate bundle file for SSL verification.
140143 Useful for self-signed certificates or private CAs.
144+ ssl_fingerprint: SHA-256 fingerprint of the server's TLS certificate
145+ as a hex string. When set, the client will verify the server's
146+ certificate fingerprint instead of performing CA-based verification.
147+ Use `async_fetch_remote_fingerprint()` to retrieve this value.
141148 use_password_obfuscation: Control password obfuscation mode (default: None).
142149 None = auto-detect (try obfuscated first, fall back to plain text).
143150 True = always use obfuscated passwords (older NanoKVM versions).
144151 False = always use plain text passwords (newer HTTPS-enabled versions).
145152 """
146153 self .url = yarl .URL (url )
147- self ._session : ClientSession | None = None
154+ self ._session : ClientSession | None = session
155+ self ._external_session_provided = session is not None
148156 self ._token = token
149157 self ._request_timeout = request_timeout
150158 self ._ws : aiohttp .ClientWebSocketResponse | None = None
151159 self ._verify_ssl = verify_ssl
152160 self ._ssl_ca_cert = ssl_ca_cert
161+ self ._ssl_fingerprint = ssl_fingerprint
153162 self ._use_password_obfuscation = use_password_obfuscation
163+ self ._ssl_config : ssl .SSLContext | Fingerprint | bool | None = None
154164
155- def _create_ssl_context (self ) -> ssl .SSLContext | bool :
165+ def _create_ssl_context (self ) -> ssl .SSLContext | Fingerprint | bool :
156166 """
157167 Create and configure SSL context based on initialization parameters.
158168
159169 Returns:
170+ Fingerprint: Certificate fingerprint pinning (when ssl_fingerprint set)
160171 ssl.SSLContext: Configured SSL context for custom certificates
161172 True: Use default SSL verification (aiohttp default)
162173 False: Disable SSL verification
@@ -166,6 +177,10 @@ def _create_ssl_context(self) -> ssl.SSLContext | bool:
166177 ssl.SSLError: If the CA certificate is invalid.
167178 """
168179
180+ if self ._ssl_fingerprint :
181+ _LOGGER .debug ("Using certificate fingerprint pinning" )
182+ return Fingerprint (bytes .fromhex (self ._ssl_fingerprint .replace (":" , "" )))
183+
169184 if not self ._verify_ssl :
170185 _LOGGER .warning (
171186 "SSL verification is disabled. This is insecure and should only be "
@@ -188,16 +203,10 @@ def token(self) -> str | None:
188203
189204 async def __aenter__ (self ) -> NanoKVMClient :
190205 """Async context manager entry."""
206+ if self ._session is None and not self ._external_session_provided :
207+ self ._session = ClientSession ()
191208
192- ssl_config = await asyncio .to_thread (self ._create_ssl_context )
193- connector = TCPConnector (ssl = ssl_config )
194- self ._session = ClientSession (connector = connector )
195-
196- _LOGGER .debug (
197- "Created client session with SSL verification: %s" ,
198- "disabled" if ssl_config is False else "enabled" ,
199- )
200-
209+ self ._ssl_config = await asyncio .to_thread (self ._create_ssl_context )
201210 return self
202211
203212 async def __aexit__ (self , exc_type : Any , exc_val : Any , exc_tb : Any ) -> None :
@@ -206,8 +215,9 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
206215 if self ._ws is not None and not self ._ws .closed :
207216 await self ._ws .close ()
208217 self ._ws = None
218+
209219 # Close HTTP session
210- if self ._session is not None :
220+ if self ._session is not None and not self . _external_session_provided :
211221 await self ._session .close ()
212222 self ._session = None
213223
@@ -221,16 +231,15 @@ async def _request(
221231 ** kwargs : Any ,
222232 ) -> AsyncIterator [ClientResponse ]:
223233 """Make an API request."""
224- assert self ._session is not None , (
225- "Client session not initialized. "
226- "Use as context manager: 'async with NanoKVMClient(url) as client:'"
227- )
228234 cookies = {}
229235 if authenticate :
230236 if not self ._token :
231237 raise NanoKVMNotAuthenticatedError ("Client is not authenticated" )
232238 cookies ["nano-kvm-token" ] = self ._token
233239
240+ assert self ._session is not None
241+ assert self ._ssl_config is not None
242+
234243 async with self ._session .request (
235244 method ,
236245 self .url / path .lstrip ("/" ),
@@ -240,6 +249,7 @@ async def _request(
240249 cookies = cookies ,
241250 timeout = aiohttp .ClientTimeout (total = self ._request_timeout ),
242251 raise_for_status = True ,
252+ ssl = self ._ssl_config ,
243253 ** kwargs ,
244254 ) as response :
245255 yield response
@@ -278,7 +288,7 @@ async def _api_request_json(
278288 async with self ._request (
279289 method ,
280290 path ,
281- json = (data .dict () if data is not None else None ),
291+ json = (data .model_dump () if data is not None else None ),
282292 ** kwargs ,
283293 ) as response :
284294 try :
@@ -795,21 +805,20 @@ async def set_mouse_jiggler_state(
795805 async def _get_ws (self ) -> aiohttp .ClientWebSocketResponse :
796806 """Get or create WebSocket connection for mouse events."""
797807 if self ._ws is None or self ._ws .closed :
798- assert self ._session is not None , (
799- "Client session not initialized. "
800- "Use as context manager: 'async with NanoKVMClient(url) as client:'"
801- )
802-
803808 if not self ._token :
804809 raise NanoKVMNotAuthenticatedError ("Client is not authenticated" )
805810
806811 # WebSocket URL uses ws:// or wss:// scheme
807812 scheme = "ws" if self .url .scheme == "http" else "wss"
808813 ws_url = self .url .with_scheme (scheme ) / "ws"
809814
815+ assert self ._session is not None
816+ assert self ._ssl_config is not None
817+
810818 self ._ws = await self ._session .ws_connect (
811819 str (ws_url ),
812820 headers = {"Cookie" : f"nano-kvm-token={ self ._token } " },
821+ ssl = self ._ssl_config ,
813822 )
814823 return self ._ws
815824
0 commit comments