Публикуемый источник
Загрузка источника ZIP
CH-J Tools
Просмотр каталогов и файлов для конкретного выпуска приложения.
CH-J / Server Manager
1704 файлы
0.1.2-beta
Добавлено: · Построено:
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 the5
//! 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.8
use crate::core::diagnostics::common::{error_result, map_limit, now_ms, service_name, DiagError, Options, Target};9
use base64::Engine;10
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};11
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};12
use rustls::{DigitallySignedStruct, SignatureScheme};13
use serde_json::{json, Value};14
use sha2::{Digest, Sha256};15
use std::net::{IpAddr, SocketAddr};16
use std::sync::Arc;17
use std::time::Duration;18
use tokio::io::{AsyncReadExt, AsyncWriteExt};19
use tokio_util::sync::CancellationToken;21
pub 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
roots30
}32
pub fn provider() -> Arc<rustls::crypto::CryptoProvider> {33
Arc::new(rustls::crypto::ring::default_provider())34
}36
/// Records the chain and the strict verdict, but lets the handshake finish for inspection.37
#[derive(Debug)]38
struct InspectingVerifier {39
inner: Arc<rustls::client::WebPkiServerVerifier>,40
chain: parking_lot::Mutex<Vec<Vec<u8>>>,41
verdict: parking_lot::Mutex<Option<Result<(), rustls::Error>>>,42
}44
impl 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
}61
}63
fn 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
}78
}80
fn 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()95
}97
fn 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")107
}109
fn 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()112
}114
pub 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 == host155
}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
})192
}194
fn 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
}199
}201
async 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
}))273
}275
pub 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
result290
}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 }))302
}304
pub 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
}))342
}344
struct ObserveKey {345
fingerprint: Arc<parking_lot::Mutex<Option<String>>>,346
}348
impl 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
}358
}360
/// Observes the SSH handshake key only; nothing is authenticated, trusted or stored.361
pub 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
}376
}378
pub 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
value400
}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 }))410
}412
/// Optional WebSocket upgrade handshake (status 101, Upgrade/Connection, accept hash).413
pub 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).await454
} else {455
exchange(tcp, &request).await456
}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 }))480
}482
#[cfg(test)]483
mod 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
}520
}SHA-256
0b6cae99e5b350adc4b323a67ab5b4d08cbb5cfc38f42bb11ee0e899ae6dd8c5CH-J Proprietary Software License 1.14 · Источник представлен в CH-J Proprietary Software License 1.14. Его наличие не изменяет лицензионных условий и не предоставляет дополнительных прав.
Архив SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f