دسترسی به شبکه محلی

ویژگی‌های وب: دسترسی به شبکه محلی

وقتی برنامه وبی که از مبدأ public (یا مبدأ اینترانت local) ارائه می‌شود به شبکه محلی خصوصی کاربر (مثل چاپگر LAN، هاب اینترنت اشیا، یا رهیاب در 192.168.1.x یا device.local) یا به سرویس همراه محلی که در دستگاه کاربر (127.0.0.1، [::1]، یا localhost) اجرا می‌شود متصل می‌شود، مرورگرها اتصال را پشت پیام‌واره اجازه دسترسی به شبکه محلی (LNA) مسدود می‌کنند.

«دسترسی به شبکه محلی» دسترسی را به دو اجازه ریزدانه و Permissions-Policy توکن براساس شبکه فضای نشانی IP (public ← local ← loopback) تقسیم می‌کند و بررسی‌ها را فقط زمانی اعمال می‌کند که درخواست از فضای نشانی کمتر خصوصی به فضای نشانی بیشتر خصوصی منتقل شود:

  • local-network: هنگام اتصال از مبدأ public به فضای نشانی local (محدوده‌های IPv4 خصوصی RFC 1918‏ 10.0.0.0/8،‏ 172.16.0.0/12،‏ 192.168.0.0/16،‏ link-local‏ 169.254.0.0/16 / fe80::/10،‏ IPv6 Unique Local Addresses‏ fc00::/7، و نام‌های میزبان mDNS‏ .local) لازم است.
  • loopback-network: هنگام اتصال از مبدأ public یا local به فضای نشانی loopback (127.0.0.0/8، ::1/128، یا localhost) الزامی است.

چون localhost (127.0.0.1 و [::1]) ازقبل در خصوصی‌ترین فضای نشانی (loopback) قرار دارد، ارائه برنامه وب شما از http://localhost درطول توسعه محلی به‌طور پیش‌فرض بررسی‌های LNA را فعال نمی‌کند. برای آزمایش کردن پیام‌واره‌های LNA و وضعیت‌های اجازه به‌صورت محلی بدون استقرار در سرور عمومی، یا فضای نشانی IPv4 و IPv6 سرور توسعه محلی‌تان را بااستفاده از پرچم Chromium --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public به public لغو کنید، یا شبکه محلی و شبکه حلقه بازگشتی را به‌صورت دستی بین Ask، Allow، و Block در تنظیمات سایت مرورگر تغییر دهید.

اجباری: هر برنامه وبی را که درخواست‌های شبکه محلی یا حلقه بازگشتی را از بافت ایمن (https://) آغاز می‌کند ارائه دهید. اجازه‌های «دسترسی به شبکه محلی» به‌طور خودکار در بافت‌های سطح بالای ناامن (http://) رد می‌شوند.

به سرصفحه‌های پیش‌پرواز «دسترسی به شبکه خصوصی» (PNA) منسوخ‌شده (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true) اتکا نکنید. «دسترسی به شبکه محلی» دسترسی را ازطریق پیام‌واره‌های اجازه کاربر سمت کارخواه (local-network و loopback-network) همراه با سرصفحه‌های CORS استاندارد (Access-Control-Allow-Origin) هنگام خواندن پاسخ‌های میان‌مبدئی اعمال می‌کند. پیش‌پروازهای PNA هرگز به‌عنوان الزامی برای مسدود کردن اجرا نشده‌اند و به‌عنوان جایگزین موردنیاز نیستند. سفت‌افزار دستگاه یا سرورهای موجود که درحال‌حاضر Access-Control-Allow-Private-Network: true را برمی‌گردانند می‌توانند بدون مشکل به این کار ادامه دهند، اما رسیدگی به PNA را به کد جدید اضافه نکنید.

مراحل پیاده‌سازی

۱. بررسی اولیه وضعیت اجازه جزئی و دروازه در اشاره کاربر

هرگز درخواست‌های شبکه محلی یا حلقه برگشتی را بدون قیدوشرط در بارگیری صفحه (DOMContentLoaded) زمانی که وضعیت اجازه 'prompt' است، فعال نکنید و هرگز اولین درخواست فعال‌کننده اجازه را از Service Worker یا Shared Worker (که فاقد Document پیوست‌شده هستند و اگر اجازه ازقبل 'granted' نباشد، فوراً ناموفق خواهند بود) شروع نکنید.

درعوض، ابتدا navigator.permissions.query() را برای 'local-network' یا 'loopback-network' پرسمان کنید:

  • اگر state === 'granted'، به‌طور خودکار متصل شود.
  • اگر state === 'prompt'، رابط کاربری واضحی نمایش دهید که توضیح دهد چرا برنامه به دسترسی به دستگاه محلی یا برنامه همراه نیاز دارد، و درخواست را ازطریق اشاره صریح کاربر (مثل کلیک کردن روی دکمه) راه‌اندازی کنید.
  • اگر state === 'denied'، میانای کاربر اصلاحی قابل‌اجرا را ارائه دهید که به کاربر آموزش دهد چگونه دسترسی به «شبکه محلی» یا «لوپ‌بک» را در تنظیمات سایت مرورگر دوباره فعال کند.
/**
 * Queries the granular Local Network Access permission for the target space.
 * @param {'local-network' | 'loopback-network'} permissionName
 */
export async function checkNetworkPermission(permissionName) {
  const status = await navigator.permissions.query({ name: permissionName });

  // Keep UI synchronized if the user changes permission in browser site settings
  status.addEventListener('change', () => {
    updatePermissionUI(permissionName, status.state);
  });

  updatePermissionUI(permissionName, status.state);
  return status.state;
}

function updatePermissionUI(permissionName, state) {
  const statusBadge = document.querySelector(`[data-permission-status="${permissionName}"]`);
  const remediationBox = document.querySelector(`[data-remediation="${permissionName}"]`);
  if (statusBadge) {
    statusBadge.textContent = state;
  }
  if (remediationBox) {
    // Show remediation instructions whenever permission is explicitly denied
    remediationBox.hidden = state !== 'denied';
  }
}

۲. اعلام targetAddressSpace در fetch() و Request

وقتی صفحه https:// درخواست نقطه پایانی http:// در شبکه محلی یا میانای برگشت حلقه می‌کند، targetAddressSpace: 'local' یا targetAddressSpace: 'loopback' را در گزینه‌های fetch() یا Request مشخص کنید.

تنظیم صریح targetAddressSpace دو کار مهم انجام می‌دهد:

  1. معافیت محتوای مختلط: بررسی‌های محتوای مختلط قبل‌از وضوح DNS اجرا می‌شود. اعلام targetAddressSpace: 'local' (یا 'loopback') پیش‌از وضوح ساناد به مرورگر اطلاع می‌دهد که نشانی وب http:// (ازجمله نام میزبان ساناد سفارشی که به IP محلی تبدیل می‌شود) نقطه پایانی محلی/ حلقه برگشتی را هدف قرار می‌دهد و پس‌از اینکه کاربر اجازه را اعطا کرد باید از مسدود کردن «محتوای مختلط» معاف شود.
  2. درستی‌سنجی فضای نشانی: پس‌از وضوح ساناد، مرورگر درستی‌سنجی می‌کند که نشانی IP وضوح‌یافته واقعاً به targetAddressSpace اعلام‌شده تعلق دارد. اگر نام میزبانی با targetAddressSpace: 'local' به‌طور غیرمنتظره‌ای به نشانی IP public تبدیل شود، مرورگر بلافاصله درخواست را با خطای شبکه TypeError لغو می‌کند.
/**
 * Connects to a local network device (e.g., LAN printer or IoT hub) over HTTP.
 * Example endpoint URL is illustrative; replace with your device's address.
 */
export async function fetchLocalDeviceStatus(deviceUrl = 'http://192.168.1.100:8080/api/status') {
  const request = new Request(deviceUrl, {
    method: 'GET',
    mode: 'cors',
    // MANDATORY: Declare 'local' when targeting RFC 1918 / .local LAN addresses
    targetAddressSpace: 'local',
  });

  const response = await fetch(request);
  if (!response.ok) {
    throw new Error(`Device responded with HTTP ${response.status}`);
  }
  return response.json();
}

/**
 * Connects to a local companion daemon running on the user's loopback interface.
 * Example loopback port is illustrative; replace with your service's port.
 */
export async function fetchLoopbackDaemon(daemonUrl = 'http://127.0.0.1:45678/health') {
  const request = new Request(daemonUrl, {
    method: 'GET',
    mode: 'cors',
    // MANDATORY: Declare 'loopback' when targeting 127.0.0.1, [::1], or localhost
    targetAddressSpace: 'loopback',
  });

  const response = await fetch(request);
  if (!response.ok) {
    throw new Error(`Companion app responded with HTTP ${response.status}`);
  }
  return response.json();
}

۳. اتصال‌های هم‌زمان با WebSocket و WebTransport

محدودیت‌های «دسترسی به شبکه محلی» بر اتصالات WebSocket و WebTransport نیز حاکم است:

  • WebSocket: سازنده WebSocket(url, protocols) فقط رشته یا آرایه زیرپروتکل را به‌عنوان آرگومان دوم می‌پذیرد—شیء گزینه‌های { targetAddressSpace } را ارسال نکنید، چون باعث ایجاد SyntaxError می‌شود. درعوض، مرورگر نشانی وب WebSocket را قبل‌از وضوح ساناد بررسی می‌کند: اتصالات ws:// که از صفحه https:// شروع شده‌اند، وقتی میزبان نشانی وب ازنظر نحوی به‌عنوان محلی یا حلقه برگشتی قابل‌تشخیص باشد (به‌ویژه IP تحت‌اللفظی خصوصی یا حلقه برگشتی (ws://192.168.1.100:8080،‏ ws://127.0.0.1:8080،‏ ws://[::1]:8080)،‏ localhost (*.localhost)، یا نام میزبان mDNS‏ .local (ws://device.local:8080)) و پشت پیام‌واره اجازه local-network یا loopback-network قرار داشته باشد، از مسدود کردن «محتوای مختلط» معاف می‌شوند. اگر نقطه پایانی محلی ازطریق نام میزبان ساناد سفارشی غیر.local آدرس‌دهی شده است، ازطریق wss:// متصل شوید.
  • WebTransport: چون WebTransport اکیداً به https:// (HTTP/3 روی QUIC با TLS یا serverCertificateHashes برای گواهی‌های خودامضاشده محلی) نیاز دارد، هرگز مسدود کردن «محتوای مختلط» را راه‌اندازی نمی‌کند و گزینه targetAddressSpace را نمی‌پذیرد. درعوض، مرورگر به‌طور خودکار فضای نشانی IP حل‌شده را درستی‌سنجی می‌کند و transport.ready را پشت پیام‌واره اجازه local-network یا loopback-network قرار می‌دهد.
/**
 * Opens a WebSocket connection to a local network device (IP literal, .local, or localhost).
 */
export function openLocalWebSocket(wsUrl = 'ws://192.168.1.100:8080/stream') {
  // Pass only subprotocol string(s) as the second argument (never an options object).
  // Browsers exempt ws:// from Mixed Content when the host is a local/loopback IP literal,
  // .local, or localhost, and gate the connection on 'local-network' or 'loopback-network'.
  const socket = new WebSocket(wsUrl, ['v1.telemetry']);
  return socket;
}

/**
 * Opens a WebTransport session to a local network endpoint using a pinned self-signed certificate hash.
 */
export async function openLocalWebTransport(wtUrl = 'https://192.168.1.100:4433/wt', certSha256Bytes) {
  const transport = new WebTransport(wtUrl, {
    serverCertificateHashes: [
      {
        algorithm: 'sha-256',
        value: certSha256Bytes,
      },
    ],
  });

  // transport.ready resolves only after the user grants the 'local-network' permission
  // and rejects with WebTransportError if the permission is denied.
  await transport.ready;
  return transport;
}

۴. Delegate Access to Cross-Origin Iframes via Permissions-Policy

به‌طور پیش‌فرض، local-network و loopback-network دارای فهرست مجاز پیش‌فرض 'self' هستند که جاسازی‌های <iframe> میان‌مبدئی را از ایجاد درخواست‌های محلی یا برگشت‌حلقه‌ای مسدود می‌کند. برای اینکه به <iframe> میان‌مبدئی جاسازی‌شده (مثل ابزاره تنظیم سخت‌افزار یا قاب SSO که با نماینده محلی ارتباط برقرار می‌کند) اجازه دهید درخواست‌های محلی یا برگشت‌به‌عقب را آغاز کند، اجازه‌های جزئی را به‌طور صریح در مشخصه allow و/یا سرایند Permissions-Policy HTTP واگذار کنید:

<!-- Delegate granular local-network and loopback-network permissions to the embedded iframe -->
<iframe
  src="https://setup.partner.example.com/device-bridge"
  allow="local-network; loopback-network"
  title="Local Device Bridge">
</iframe>

سرایند Permissions-Policy را در پاسخ سند سطح بالا (جاسازی) ارسال کنید، نه در پاسخ iframe:

Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")

راهبردهای جایگزین

اگر هدف «خط پایه» شما از اجازه‌های «دسترسی به شبکه محلی» و گزینه‌های targetAddressSpace پشتیبانی نمی‌کند، تشخیص ویژگی با بازگشت تدریجی را پیاده‌سازی کنید:

  1. Permission Query Fallback: Wrap navigator.permissions.query({ name: permissionName }) in a try / catch. اگر مرورگر TypeError را به‌دلیل اینکه 'local-network' یا 'loopback-network' یک PermissionName ناشناخته است رد می‌کند، وضعیت را به‌عنوان 'prompt' درنظر بگیرید تا درخواست همچنان با کلیک صریح کاربر اجرا شود (خود تلاش برای اتصال، پیام‌واره مرورگر را در جایی که LNA پشتیبانی می‌شود فعال می‌کند).
  2. نام اجازه ترکیبی قدیمی { name: 'local-network-access' } را پُرسمان نکنید. در نسخه‌های قدیمی‌تر Chrome، این پُرسمان فرایند پردازنده را ازکار می‌اندازد و try / catch نمی‌تواند از آن جلوگیری کند. نسخه‌های جدیدتر Chrome با 'local-network-access' فقط به‌عنوان نام مستعار قدیمی برای اجازه‌های جزئی رفتار می‌کنند.
  3. مدیریت رد کردن با احترام: همیشه fetch()، رویدادهای خطای WebSocket، و transport.ready را در مدیریت‌کننده‌های خطا قرار دهید که TypeError / قطعی اتصال را شناسایی می‌کنند و مراحل اصلاح واضحی را ارائه می‌دهند (بررسی اینکه دستگاه محلی روشن است، به همان شبکه متصل است، و در اجازه‌های مرورگر مجاز است).
/**
 * Cross-browser permission check for the granular LNA permissions.
 * Never falls back to querying 'local-network-access', which crashes older Chrome versions.
 * @param {'local-network' | 'loopback-network'} permissionName
 */
export async function queryLnaPermissionSafe(permissionName) {
  if (!navigator.permissions?.query) {
    return 'prompt';
  }
  try {
    const status = await navigator.permissions.query({ name: permissionName });
    return status.state;
  } catch {
    // Unrecognized permission name; proceed on user gesture
    return 'prompt';
  }
}