CH-J ToolsZarządzanie serwerem przez SSH
Menu
Opublikowane źródło

CH-J Tools

Przeglądaj katalogi i pliki dla konkretnego wydania aplikacji.

Pobierz źródło ZIP
CH-J / Server Manager 1704 pliki
0.1.2-beta Dodano: · Built:
socket.rs Dodano: Data niezanotowana 26,0 KB · 520 linie
Kod Pobierz plik PDF
1//! TCP, TLS, WebSocket and SSH identification probes (port of `socketDiagnostics.js`).
2//!
3//! The TLS probe is inspection-only: it records the presented chain even when it is invalid,
4//! but a `valid` result requires both a trusted chain (Mozilla roots via webpki-roots, as the
5//! Node runtime of 0.1.0 used its bundled Mozilla store) and hostname/IP SAN verification.
6//! No application data is sent on that socket.
8use crate::core::diagnostics::common::{error_result, map_limit, now_ms, service_name, DiagError, Options, Target};
9use base64::Engine;
10use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
11use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
12use rustls::{DigitallySignedStruct, SignatureScheme};
13use serde_json::{json, Value};
14use sha2::{Digest, Sha256};
15use std::net::{IpAddr, SocketAddr};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio_util::sync::CancellationToken;
21pub fn root_store(extra_ca: Option<&[u8]>) -> rustls::RootCertStore {
22 let mut roots = rustls::RootCertStore::empty();
23 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
24 if let Some(pem) = extra_ca {
25 for cert in rustls_pemfile::certs(&mut &pem[..]).flatten() {
26 let _ = roots.add(cert);
27 }
28 }
29 roots
32pub fn provider() -> Arc<rustls::crypto::CryptoProvider> {
33 Arc::new(rustls::crypto::ring::default_provider())
36/// Records the chain and the strict verdict, but lets the handshake finish for inspection.
37#[derive(Debug)]
38struct InspectingVerifier {
39 inner: Arc<rustls::client::WebPkiServerVerifier>,
40 chain: parking_lot::Mutex<Vec<Vec<u8>>>,
41 verdict: parking_lot::Mutex<Option<Result<(), rustls::Error>>>,
44impl ServerCertVerifier for InspectingVerifier {
45 fn verify_server_cert(&self, end_entity: &CertificateDer<'_>, intermediates: &[CertificateDer<'_>], server_name: &ServerName<'_>, ocsp: &[u8], now: UnixTime) -> Result<ServerCertVerified, rustls::Error> {
46 let mut chain = vec![end_entity.to_vec()];
47 chain.extend(intermediates.iter().map(|c| c.to_vec()));
48 *self.chain.lock() = chain;
49 *self.verdict.lock() = Some(self.inner.verify_server_cert(end_entity, intermediates, server_name, ocsp, now).map(|_| ()));
50 Ok(ServerCertVerified::assertion())
51 }
52 fn verify_tls12_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
53 self.inner.verify_tls12_signature(message, cert, dss)
54 }
55 fn verify_tls13_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> Result<HandshakeSignatureValid, rustls::Error> {
56 self.inner.verify_tls13_signature(message, cert, dss)
57 }
58 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
59 self.inner.supported_verify_schemes()
60 }
63fn node_error_code(error: &rustls::Error) -> String {
64 use rustls::CertificateError as C;
65 match error {
66 rustls::Error::InvalidCertificate(certificate) => match certificate {
67 C::Expired | C::ExpiredContext { .. } => "CERT_HAS_EXPIRED",
68 C::NotValidYet | C::NotValidYetContext { .. } => "CERT_NOT_YET_VALID",
69 C::UnknownIssuer => "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
70 C::NotValidForName | C::NotValidForNameContext { .. } => "ERR_TLS_CERT_ALTNAME_INVALID",
71 C::BadSignature => "CERT_SIGNATURE_FAILURE",
72 C::Revoked => "CERT_REVOKED",
73 _ => "UNTRUSTED_CHAIN",
74 }
75 .to_string(),
76 _ => "UNTRUSTED_CHAIN".to_string(),
77 }
80fn openssl_name(oid: &str) -> String {
81 match oid {
82 "1.2.840.113549.1.1.11" => "sha256WithRSAEncryption",
83 "1.2.840.113549.1.1.12" => "sha384WithRSAEncryption",
84 "1.2.840.113549.1.1.13" => "sha512WithRSAEncryption",
85 "1.2.840.113549.1.1.5" => "sha1WithRSAEncryption",
86 "1.2.840.113549.1.1.10" => "rsassaPss",
87 "1.2.840.10045.4.3.2" => "ecdsa-with-SHA256",
88 "1.2.840.10045.4.3.3" => "ecdsa-with-SHA384",
89 "1.2.840.10045.4.3.4" => "ecdsa-with-SHA512",
90 "1.3.101.112" => "ED25519",
91 "1.3.101.113" => "ED448",
92 other => other,
93 }
94 .to_string()
97fn node_name(name: &x509_parser::x509::X509Name<'_>) -> String {
98 let mut parts = Vec::new();
99 for rdn in name.iter() {
100 for attribute in rdn.iter() {
101 let key = x509_parser::objects::oid2abbrev(attribute.attr_type(), x509_parser::objects::oid_registry()).map(str::to_string).unwrap_or_else(|_| attribute.attr_type().to_id_string());
102 let value = attribute.as_str().map(str::to_string).unwrap_or_else(|_| hex::encode(attribute.as_slice()));
103 parts.push(format!("{key}={value}"));
104 }
105 }
106 parts.join("\n")
109fn node_date(time: x509_parser::time::ASN1Time) -> String {
110 let date = chrono::DateTime::from_timestamp(time.timestamp(), 0).unwrap_or_default();
111 date.format("%b %e %H:%M:%S %Y GMT").to_string()
114pub fn certificate_info(der: &[u8], host: &str) -> Value {
115 use x509_parser::prelude::*;
116 let fingerprint = Sha256::digest(der).iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(":");
117 let Ok((_, cert)) = X509Certificate::from_der(der) else {
118 return json!({ "fingerprint256": fingerprint, "error": "CERTIFICATE_UNPARSEABLE" });
119 };
120 let mut san: Vec<String> = Vec::new();
121 let mut dns_names: Vec<String> = Vec::new();
122 let mut ip_names: Vec<IpAddr> = Vec::new();
123 if let Ok(Some(extension)) = cert.subject_alternative_name() {
124 for name in &extension.value.general_names {
125 match name {
126 GeneralName::DNSName(dns) => {
127 san.push(format!("DNS:{dns}"));
128 dns_names.push(dns.to_lowercase());
129 }
130 GeneralName::IPAddress(bytes) => {
131 let ip = match bytes.len() {
132 4 => Some(IpAddr::from(<[u8; 4]>::try_from(*bytes).unwrap())),
133 16 => Some(IpAddr::from(<[u8; 16]>::try_from(*bytes).unwrap())),
134 _ => None,
135 };
136 if let Some(ip) = ip {
137 san.push(format!("IP Address:{}", if ip.is_ipv6() { ip.to_string().to_uppercase() } else { ip.to_string() }));
138 ip_names.push(ip);
139 }
140 }
141 GeneralName::RFC822Name(email) => san.push(format!("email:{email}")),
142 GeneralName::URI(uri) => san.push(format!("URI:{uri}")),
143 _ => {}
144 }
145 }
146 }
147 let host_matches = match host.parse::<IpAddr>() {
148 Ok(ip) => ip_names.contains(&ip),
149 Err(_) => dns_names.iter().any(|pattern| {
150 let host = host.to_lowercase();
151 if let Some(suffix) = pattern.strip_prefix("*.") {
152 host.split_once('.').map(|(label, rest)| !label.is_empty() && rest == suffix).unwrap_or(false)
153 } else {
154 *pattern == host
155 }
156 }),
157 };
158 let (key_type, bits, curve) = match cert.public_key().parsed() {
159 Ok(x509_parser::public_key::PublicKey::RSA(rsa)) => ("rsa", json!(rsa.key_size()), Value::Null),
160 Ok(x509_parser::public_key::PublicKey::EC(point)) => {
161 let curve_oid = cert.public_key().algorithm.parameters.as_ref().and_then(|p| p.as_oid().ok()).map(|o| o.to_id_string()).unwrap_or_default();
162 let (name, bits) = match curve_oid.as_str() {
163 "1.2.840.10045.3.1.7" => ("prime256v1", 256),
164 "1.3.132.0.34" => ("secp384r1", 384),
165 "1.3.132.0.35" => ("secp521r1", 521),
166 _ => ("", point.key_size()),
167 };
168 ("ec", json!(bits), if name.is_empty() { Value::Null } else { json!(name) })
169 }
170 Ok(x509_parser::public_key::PublicKey::Unknown(_)) if cert.public_key().algorithm.algorithm.to_id_string() == "1.3.101.112" => ("ed25519", Value::Null, Value::Null),
171 _ => ("unknown", Value::Null, Value::Null),
172 };
173 let not_after = cert.validity().not_after.timestamp();
174 let days = ((not_after * 1000 - chrono::Utc::now().timestamp_millis()) as f64 / 86_400_000.0).floor();
175 let self_signed = cert.subject() == cert.issuer() && cert.verify_signature(None).is_ok();
176 json!({
177 "subject": node_name(cert.subject()),
178 "issuer": node_name(cert.issuer()),
179 "san": if san.is_empty() { Value::Null } else { json!(san.join(", ")) },
180 "validFrom": node_date(cert.validity().not_before),
181 "validUntil": node_date(cert.validity().not_after),
182 "daysUntilExpiration": days,
183 "fingerprint256": fingerprint,
184 "publicKeyType": key_type,
185 "publicKeyBits": bits,
186 "publicKeyCurve": curve,
187 "signatureAlgorithm": openssl_name(&cert.signature_algorithm.algorithm.to_id_string()),
188 "hostnameMatches": host_matches,
189 "selfSigned": self_signed,
190 "serialNumber": cert.raw_serial().iter().map(|b| format!("{b:02X}")).collect::<String>()
191 })
194fn server_name(host: &str) -> ServerName<'static> {
195 match host.parse::<IpAddr>() {
196 Ok(ip) => ServerName::IpAddress(ip.into()),
197 Err(_) => ServerName::try_from(host.to_string()).unwrap_or_else(|_| ServerName::IpAddress(IpAddr::from([0, 0, 0, 0]).into())),
198 }
201async fn tls_probe(target: &Target, address: &str, options: &Options, cancel: &CancellationToken, version: &'static str, ca: Option<&[u8]>) -> Result<Value, DiagError> {
202 let started = now_ms();
203 let roots = Arc::new(root_store(ca));
204 let inner = rustls::client::WebPkiServerVerifier::builder_with_provider(roots, provider()).build().map_err(|e| DiagError::with_message("TLS_SETUP_FAILED", e.to_string()))?;
205 let verifier = Arc::new(InspectingVerifier { inner, chain: Default::default(), verdict: Default::default() });
206 let versions: &[&'static rustls::SupportedProtocolVersion] = if version == "TLSv1.2" { &[&rustls::version::TLS12] } else { &[&rustls::version::TLS13] };
207 let mut config = rustls::ClientConfig::builder_with_provider(provider())
208 .with_protocol_versions(versions)
209 .map_err(|e| DiagError::with_message("TLS_SETUP_FAILED", e.to_string()))?
210 .dangerous()
211 .with_custom_certificate_verifier(verifier.clone())
212 .with_no_client_auth();
213 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
214 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
215 let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
216 let work = async {
217 let tcp = tokio::net::TcpStream::connect(SocketAddr::new(ip, target.port)).await.map_err(DiagError::from_io)?;
218 let name = if target.host.parse::<IpAddr>().is_ok() { server_name(&target.host) } else { server_name(&target.host) };
219 connector.connect(name, tcp).await.map_err(|e| {
220 let text = e.to_string();
221 if text.contains("PeerIncompatible") || text.contains("protocol version") || text.contains("ProtocolVersion") {
222 DiagError::with_message("ERR_SSL_UNSUPPORTED_PROTOCOL", format!("unsupported protocol: {text}"))
223 } else {
224 DiagError::with_message("TLS_HANDSHAKE_FAILED", text)
225 }
226 })
227 };
228 let stream = tokio::select! {
229 result = tokio::time::timeout(Duration::from_millis(options.timeout_ms), work) => result.map_err(|_| DiagError::new("TIMEOUT"))??,
230 _ = cancel.cancelled() => return Err(DiagError::new("CANCELLED")),
231 };
232 let (_, session) = stream.get_ref();
233 let negotiated = match session.protocol_version() {
234 Some(rustls::ProtocolVersion::TLSv1_2) => "TLSv1.2",
235 Some(rustls::ProtocolVersion::TLSv1_3) => "TLSv1.3",
236 _ => "unknown",
237 };
238 let cipher = session.negotiated_cipher_suite().map(|s| {
239 let name = format!("{:?}", s.suite());
240 json!({ "name": name, "standardName": name, "version": negotiated })
241 });
242 let alpn = session.alpn_protocol().map(|p| String::from_utf8_lossy(p).to_string());
243 let chain_der = verifier.chain.lock().clone();
244 let chain: Vec<Value> = chain_der.iter().take(12).map(|der| certificate_info(der, &target.host)).collect();
245 let mut errors: Vec<String> = Vec::new();
246 match verifier.verdict.lock().clone() {
247 Some(Ok(())) => {}
248 Some(Err(error)) => {
249 let code = node_error_code(&error);
250 errors.push(code);
251 }
252 None => errors.push("CERTIFICATE_MISSING".into()),
253 }
254 if chain.is_empty() && !errors.iter().any(|e| e == "CERTIFICATE_MISSING") {
255 errors.push("CERTIFICATE_MISSING".into());
256 }
257 let days = chain.first().and_then(|c| c["daysUntilExpiration"].as_f64()).unwrap_or(0.0);
258 Ok(json!({
259 "status": if !errors.is_empty() { "invalid" } else if days < 30.0 { "warning" } else { "valid" },
260 "valid": errors.is_empty(),
261 "requestedVersion": version,
262 "negotiatedVersion": negotiated,
263 "address": address,
264 "port": target.port,
265 "sni": if target.host.parse::<IpAddr>().is_ok() { Value::Null } else { json!(target.host) },
266 "durationMs": now_ms() - started,
267 "cipher": cipher,
268 "alpn": alpn,
269 "chain": chain,
270 "errors": errors,
271 "validation": "rustls/webpki trust/date/signature verification (Mozilla root store) plus explicit hostname verification; revocation/CT not independently audited."
272 }))
275pub async fn run_tls(target: &Target, address: &str, options: &Options, cancel: &CancellationToken, progress: Arc<dyn Fn(Value) + Send + Sync>, ca: Option<&[u8]>) -> Result<Value, DiagError> {
276 let mut results = Vec::new();
277 for version in ["TLSv1.2", "TLSv1.3"] {
278 let result = match tls_probe(target, address, options, cancel, version, ca).await {
279 Ok(result) => result,
280 Err(error) => {
281 if cancel.is_cancelled() {
282 return Err(error);
283 }
284 let mut result = error_result(&error);
285 result["requestedVersion"] = json!(version);
286 if regex::Regex::new(r"(?i)protocol version|unsupported protocol").unwrap().is_match(&error.message) {
287 result["status"] = json!("unsupported");
288 }
289 result
290 }
291 };
292 let mut event = json!({ "kind": "tls", "address": address });
293 if let (Some(e), Some(r)) = (event.as_object_mut(), result.as_object()) {
294 e.extend(r.clone());
295 }
296 progress(event);
297 results.push(result);
298 }
299 let any_invalid = results.iter().any(|r| r["status"] == "invalid");
300 let any_valid = results.iter().any(|r| r["valid"] == true);
301 Ok(json!({ "status": if any_invalid { if any_valid { "warning" } else { "failed" } } else if any_valid { "success" } else { "warning" }, "results": results }))
304pub async fn tcp_probe(address: &str, port: u16, options: &Options, cancel: &CancellationToken) -> Result<Value, DiagError> {
305 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
306 let started = now_ms();
307 let mut connected_ms: Option<f64> = None;
308 let mut banner: Vec<u8> = Vec::new();
309 let work = async {
310 let mut stream = tokio::net::TcpStream::connect(SocketAddr::new(ip, port)).await.map_err(DiagError::from_io)?;
311 connected_ms = Some(now_ms() - started);
312 if [21u16, 22, 25, 110, 143, 587].contains(&port) {
313 let mut buffer = [0u8; 512];
314 if let Ok(Ok(n)) = tokio::time::timeout(Duration::from_millis(500.min(options.timeout_ms)), stream.read(&mut buffer)).await {
315 banner.extend_from_slice(&buffer[..n]);
316 }
317 }
318 Ok::<_, DiagError>(())
319 };
320 let outcome = tokio::select! {
321 result = tokio::time::timeout(Duration::from_millis(options.timeout_ms), work) => match result { Ok(r) => r, Err(_) => Err(DiagError::new("TIMEOUT")) },
322 _ = cancel.cancelled() => return Err(DiagError::new("CANCELLED")),
323 };
324 let (status, code) = match &outcome {
325 Ok(()) => ("connected", Value::Null),
326 Err(e) if e.code == "ECONNREFUSED" => ("refused", json!(e.code)),
327 Err(e) if e.code == "TIMEOUT" || e.code == "ETIMEDOUT" => ("timeout", json!(e.code)),
328 Err(e) => ("error", json!(e.code)),
329 };
330 let banner_text: String = String::from_utf8_lossy(&banner).chars().filter(|c| !matches!(*c as u32, 0..=8 | 11..=31 | 127)).take(512).collect();
331 Ok(json!({
332 "address": address,
333 "family": if ip.is_ipv4() { 4 } else { 6 },
334 "port": port,
335 "standardService": service_name(port),
336 "status": status,
337 "code": code,
338 "connectionMs": connected_ms,
339 "banner": if banner_text.is_empty() { Value::Null } else { json!(banner_text) },
340 "serviceNote": "Standard port assignment is a label, not service verification."
341 }))
344struct ObserveKey {
345 fingerprint: Arc<parking_lot::Mutex<Option<String>>>,
348impl russh::client::Handler for ObserveKey {
349 type Error = russh::Error;
350 async fn check_server_key(&mut self, key: &russh::keys::PublicKeyOrCertificate) -> Result<bool, Self::Error> {
351 let blob = match key {
352 russh::keys::PublicKeyOrCertificate::PublicKey { key, .. } => key.to_bytes()?,
353 russh::keys::PublicKeyOrCertificate::Certificate(c) => c.to_bytes()?,
354 };
355 *self.fingerprint.lock() = Some(format!("SHA256:{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(Sha256::digest(&blob))));
356 Ok(false)
357 }
360/// Observes the SSH handshake key only; nothing is authenticated, trusted or stored.
361pub async fn ssh_identification(address: &str, options: &Options, cancel: &CancellationToken) -> Result<Value, DiagError> {
362 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
363 let fingerprint = Arc::new(parking_lot::Mutex::new(None));
364 let handler = ObserveKey { fingerprint: fingerprint.clone() };
365 let config = Arc::new(russh::client::Config::default());
366 let attempt = russh::client::connect(config, SocketAddr::new(ip, 22), handler);
367 let _ = tokio::select! {
368 result = tokio::time::timeout(Duration::from_millis(options.timeout_ms), attempt) => result.map_err(|_| DiagError::new("TIMEOUT")),
369 _ = cancel.cancelled() => return Err(DiagError::new("CANCELLED")),
370 };
371 let observed = fingerprint.lock().clone();
372 match observed {
373 Some(fp) => Ok(json!({ "status": "success", "fingerprint": fp, "identification": null, "authenticated": false, "note": "Observed handshake key; not trusted or added to SSH known hosts." })),
374 None => Err(DiagError::with_message("SSH_HANDSHAKE_FAILED", "The SSH handshake did not present a host key.")),
375 }
378pub async fn run_tcp(address: &str, options: &Options, cancel: &CancellationToken, progress: Arc<dyn Fn(Value) + Send + Sync>) -> Result<Value, DiagError> {
379 let ports = map_limit(options.ports.clone(), 4, |port| {
380 let progress = progress.clone();
381 async move {
382 let result = tcp_probe(address, port, options, cancel).await?;
383 let mut event = json!({ "kind": "tcp" });
384 if let (Some(e), Some(r)) = (event.as_object_mut(), result.as_object()) {
385 e.extend(r.clone());
386 }
387 progress(event);
388 Ok(result)
389 }
390 })
391 .await?;
392 let mut ssh = Value::Null;
393 if ports.iter().any(|p| p["port"] == 22 && p["status"] == "connected") {
394 ssh = match ssh_identification(address, options, cancel).await {
395 Ok(mut value) => {
396 if value["identification"].is_null() {
397 value["identification"] = ports.iter().find(|p| p["port"] == 22).map(|p| p["banner"].clone()).unwrap_or(Value::Null);
398 }
399 value
400 }
401 Err(error) => {
402 if cancel.is_cancelled() {
403 return Err(error);
404 }
405 error_result(&error)
406 }
407 };
408 }
409 Ok(json!({ "status": if ports.iter().any(|p| p["status"] == "connected") { "success" } else { "warning" }, "ports": ports, "ssh": ssh }))
412/// Optional WebSocket upgrade handshake (status 101, Upgrade/Connection, accept hash).
413pub async fn websocket_probe(target: &Target, address: &str, options: &Options, cancel: &CancellationToken, ca: Option<&[u8]>) -> Result<Value, DiagError> {
414 let url = url::Url::parse(&target.url).map_err(|_| DiagError::new("INVALID_URL"))?;
415 let secure = matches!(url.scheme(), "wss" | "https");
416 let ip: IpAddr = address.parse().map_err(|_| DiagError::new("INVALID_IP"))?;
417 let mut key_bytes = [0u8; 16];
418 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut key_bytes);
419 let key = base64::engine::general_purpose::STANDARD.encode(key_bytes);
420 let started = now_ms();
421 let host_header = match url.port() {
422 Some(port) => format!("{}:{port}", url.host_str().unwrap_or("")),
423 None => url.host_str().unwrap_or("").to_string(),
424 };
425 let path = format!("{}{}", url.path(), url.query().map(|q| format!("?{q}")).unwrap_or_default());
426 let request = format!("GET {path} HTTP/1.1\r\nHost: {host_header}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: {key}\r\n\r\n");
427 async fn exchange<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin>(mut stream: S, request: &str) -> Result<String, DiagError> {
428 stream.write_all(request.as_bytes()).await.map_err(DiagError::from_io)?;
429 let mut head = Vec::new();
430 let mut buffer = [0u8; 1024];
431 while !head.windows(4).any(|w| w == b"\r\n\r\n") {
432 let n = stream.read(&mut buffer).await.map_err(DiagError::from_io)?;
433 if n == 0 {
434 break;
435 }
436 head.extend_from_slice(&buffer[..n]);
437 if head.len() > 16384 {
438 return Err(DiagError::new("HEADER_LIMIT"));
439 }
440 }
441 Ok(String::from_utf8_lossy(&head).to_string())
442 }
443 let work = async {
444 let tcp = tokio::net::TcpStream::connect(SocketAddr::new(ip, target.port)).await.map_err(DiagError::from_io)?;
445 if secure {
446 let mut config = rustls::ClientConfig::builder_with_provider(provider())
447 .with_safe_default_protocol_versions()
448 .map_err(|e| DiagError::with_message("TLS_SETUP_FAILED", e.to_string()))?
449 .with_root_certificates(root_store(ca))
450 .with_no_client_auth();
451 config.alpn_protocols = vec![b"http/1.1".to_vec()];
452 let stream = tokio_rustls::TlsConnector::from(Arc::new(config)).connect(server_name(&target.host), tcp).await.map_err(|e| DiagError::with_message("TLS_CERT_INVALID", e.to_string()))?;
453 exchange(stream, &request).await
454 } else {
455 exchange(tcp, &request).await
456 }
457 };
458 let head = tokio::select! {
459 result = tokio::time::timeout(Duration::from_millis(options.timeout_ms), work) => result.map_err(|_| DiagError::new("TIMEOUT"))??,
460 _ = cancel.cancelled() => return Err(DiagError::new("CANCELLED")),
461 };
462 let mut lines = head.split("\r\n");
463 let status_code: u16 = lines.next().and_then(|l| l.split_whitespace().nth(1)).and_then(|s| s.parse().ok()).unwrap_or(0);
464 let mut headers = std::collections::HashMap::new();
465 for line in lines {
466 if let Some((k, v)) = line.split_once(':') {
467 headers.insert(k.trim().to_lowercase(), v.trim().to_string());
468 }
469 }
470 if status_code != 101 {
471 return Ok(json!({ "status": "unsupported", "statusCode": status_code, "durationMs": now_ms() - started, "address": address }));
472 }
473 let mut hasher = sha1::Sha1::new();
474 sha1::Digest::update(&mut hasher, format!("{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11").as_bytes());
475 let expected = base64::engine::general_purpose::STANDARD.encode(sha1::Digest::finalize(hasher));
476 let valid = headers.get("upgrade").map(|v| v.to_lowercase() == "websocket").unwrap_or(false)
477 && regex::Regex::new(r"(?i)\bupgrade\b").unwrap().is_match(headers.get("connection").map(String::as_str).unwrap_or(""))
478 && headers.get("sec-websocket-accept") == Some(&expected);
479 Ok(json!({ "status": if valid { "success" } else { "failed" }, "valid": valid, "statusCode": status_code, "durationMs": now_ms() - started, "address": address, "secure": secure }))
482#[cfg(test)]
483mod tests {
484 use super::*;
486 const SERVER_PEM: &str = include_str!("../../../../tools/test-fixtures/diagnostics/server.pem");
487 const EXPIRED_PEM: &str = include_str!("../../../../tools/test-fixtures/diagnostics/expired.pem");
489 fn first_der(pem: &str) -> Vec<u8> {
490 rustls_pemfile::certs(&mut pem.as_bytes()).next().unwrap().unwrap().to_vec()
491 }
493 #[test]
494 fn certificate_details() {
495 let info = certificate_info(&first_der(SERVER_PEM), "localhost");
496 assert_eq!(info["hostnameMatches"], true);
497 assert!(info["san"].as_str().unwrap().contains("DNS:localhost"));
498 assert!(info["daysUntilExpiration"].as_f64().unwrap() > 1000.0);
499 assert_eq!(certificate_info(&first_der(SERVER_PEM), "127.0.0.1")["hostnameMatches"], true);
500 assert_eq!(certificate_info(&first_der(SERVER_PEM), "example.com")["hostnameMatches"], false);
501 assert!(certificate_info(&first_der(EXPIRED_PEM), "localhost")["daysUntilExpiration"].as_f64().unwrap() < 0.0);
502 }
504 #[tokio::test]
505 async fn tcp_refused_and_connected() {
506 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
507 let port = listener.local_addr().unwrap().port();
508 tokio::spawn(async move {
509 let _ = listener.accept().await;
510 });
511 // Windows retries SYN to a closed local port for about 2 s before reporting the refusal.
512 let options = Options { timeout_ms: 10_000, ..Default::default() };
513 let cancel = CancellationToken::new();
514 assert_eq!(tcp_probe("127.0.0.1", port, &options, &cancel).await.unwrap()["status"], "connected");
515 let closed = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
516 let closed_port = closed.local_addr().unwrap().port();
517 drop(closed);
518 assert_eq!(tcp_probe("127.0.0.1", closed_port, &options, &cancel).await.unwrap()["status"], "refused");
519 }
SHA-2560b6cae99e5b350adc4b323a67ab5b4d08cbb5cfc38f42bb11ee0e899ae6dd8c5

CH-J Proprietary Software License 1.14 · Źródło jest dostarczane pod CH-J Proprietary Software License 1.14. Jego dostępność nie zmienia warunków licencji ani nie przyznaje dodatkowych praw.

SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Pliki