স্থানীয় নেটওয়ার্ক অ্যাক্সেস

যখন কোনও ওয়েব অ্যাপ্লিকেশন public অরিজিন (বা ইন্ট্রানেট local অরিজিন) থেকে ব্যবহারকারীর ব্যক্তিগত লোকাল নেটওয়ার্ক (যেমন, LAN প্রিন্টার, IoT হাব বা 192.168.1.x বা device.local-এ রাউটার) বা ব্যবহারকারীর মেশিনে চলা লোকাল কম্প্যানিয়ন পরিষেবার (127.0.0.1, [::1] বা localhost) সাথে কানেক্ট করে, তখন ব্রাউজার লোকাল নেটওয়ার্ক অ্যাক্সেস (LNA) অনুমতি প্রম্পটের মাধ্যমে কানেকশন গেট করে।

লোকাল নেটওয়ার্ক অ্যাক্সেস, IP অ্যাড্রেস স্পেস ল্যাটিসের (public → local → loopback) উপর ভিত্তি করে অ্যাক্সেসকে দুটি গ্র্যানুলার অনুমতি এবং Permissions-Policy টোকেনে ভাগ করে, শুধুমাত্র তখনই চেক এনফোর্স করে যখন কোনও অনুরোধ কম ব্যক্তিগত অ্যাড্রেস স্পেস থেকে আরও ব্যক্তিগত অ্যাড্রেস স্পেসে যায়:

  • local-network: public অরিজিন থেকে local অ্যাড্রেস স্পেসে (RFC 1918 ব্যক্তিগত IPv4 রেঞ্জ 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, লিঙ্ক-লোকাল 169.254.0.0/16 / fe80::/10, IPv6 অনন্য লোকাল অ্যাড্রেস fc00::/7 এবং .local mDNS হোস্টনেম) কানেক্ট করার সময় প্রয়োজন।
  • loopback-network: public বা local সোর্স থেকে loopback অ্যাড্রেস স্পেসে (127.0.0.0/8, ::1/128 বা localhost) কানেক্ট করার সময় প্রয়োজন।

যেহেতু localhost (127.0.0.1 এবং [::1]) আগে থেকেই সবচেয়ে ব্যক্তিগত (loopback) অ্যাড্রেস স্পেসে রয়েছে, তাই লোকাল ডেভেলপমেন্টের সময় http://localhost থেকে আপনার ওয়েব অ্যাপ পরিবেশন করলে ডিফল্ট হিসেবে LNA চেক ট্রিগার করবে না। পাবলিক সার্ভারে ডেপ্লয় না করে, স্থানীয়ভাবে LNA প্রম্পট ও অনুমতির স্ট্যাটাস পরীক্ষা করতে, হয় --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public Chromium ফ্ল্যাগ ব্যবহার করে আপনার লোকাল ডেভ সার্ভারের IPv4 ও IPv6 অ্যাড্রেস স্পেস ওভাররাইড করে public করুন অথবা ব্রাউজারের সাইট সেটিংস থেকে Ask, Allow ও Block-এর মধ্যে লোকাল নেটওয়ার্ক ও লুপব্যাক নেটওয়ার্ক ম্যানুয়ালি টগল করুন।

বাধ্যতামূলক: সুরক্ষিত কনটেক্সট (https://) থেকে লোকাল বা লুপব্যাক নেটওয়ার্ক অনুরোধ শুরু করে এমন যেকোনও ওয়েব অ্যাপ্লিকেশন পরিবেশন করুন। অসুরক্ষিত (http://) টপ-লেভেল কনটেক্সটে লোকাল নেটওয়ার্ক অ্যাক্সেসের অনুমতি অটোমেটিক প্রত্যাখ্যান করা হয়।

বন্ধ হয়ে যাওয়া প্রাইভেট নেটওয়ার্ক অ্যাক্সেস (PNA) প্রিফ্লাইট হেডারের (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true) উপর নির্ভর করবেন না। ক্রস-অরিজিন উত্তর পড়ার সময় লোকাল নেটওয়ার্ক অ্যাক্সেস, স্ট্যান্ডার্ড CORS হেডারের (Access-Control-Allow-Origin) সাথে ক্লায়েন্ট-সাইড ব্যবহারকারীর অনুমতি সংক্রান্ত প্রম্পটের (local-network এবং loopback-network) মাধ্যমে অ্যাক্সেস এনফোর্স করে। PNA প্রিফ্লাইট কখনও ব্লকিংয়ের প্রয়োজনীয়তা হিসেবে এনফোর্স করা হয়নি এবং ফলব্যাক হিসেবেও প্রয়োজন নেই। আগে থেকে থাকা সার্ভার বা ডিভাইস ফার্মওয়্যার যা আগে থেকেই Access-Control-Allow-Private-Network: true রিটার্ন করে, তা কোনও ক্ষতি না করেই তা করতে পারে, তবে নতুন কোডে PNA হ্যান্ডলিং যোগ করবেন না।

প্রয়োগ করার বিভিন্ন ধাপ

১. ইউজার জেসচারে গ্র্যানুলার অনুমতির স্ট্যাটাস ও গেট প্রি-চেক করুন

অনুমতির স্ট্যাটাস 'prompt' হলে, পৃষ্ঠা লোড হওয়ার (DOMContentLoaded) সময় কখনও শর্তহীনভাবে লোকাল নেটওয়ার্ক বা লুপব্যাক অনুরোধ ফায়ার করবেন না এবং Service Worker বা Shared Worker (যেগুলিতে অ্যাটাচ করা Document থাকে না এবং অনুমতি আগে থেকে 'granted' না থাকলে অবিলম্বে ব্যর্থ হবে) থেকে কখনও প্রথম অনুমতি-ট্রিগারকারী অনুরোধ শুরু করবেন না।

পরিবর্তে, প্রথমে 'local-network' বা 'loopback-network'-এর জন্য navigator.permissions.query() কোয়েরি করুন:

  • state === 'granted' হলে, অটোমেটিক কানেক্ট করুন।
  • state === 'prompt' হলে, অ্যাপের কেন স্থানীয় ডিভাইস বা কম্প্যানিয়ন অ্যাপে অ্যাক্সেস প্রয়োজন তা ব্যাখ্যা করে স্পষ্ট UI দেখান এবং ব্যবহারকারীর স্পষ্ট জেসচার (যেমন, বোতামে ক্লিক করা) থেকে অনুরোধ ট্রিগার করুন।
  • state === 'denied' হলে, ব্রাউজার সাইট সেটিংসে কীভাবে লোকাল নেটওয়ার্ক বা লুপব্যাক অ্যাক্সেস আবার চালু করতে হবে সেই বিষয়ে ব্যবহারকারীকে নির্দেশ দিয়ে অ্যাকশনেবল রেমিডিয়েশন UI রেন্ডার করুন।
/**
 * 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';
  }
}

২. fetch() এবং Request-এ targetAddressSpace ঘোষণা করুন

কোনও https:// পৃষ্ঠা স্থানীয় নেটওয়ার্ক বা লুপব্যাক ইন্টারফেসে http:// এন্ডপয়েন্টের অনুরোধ করলে, fetch() বা Request বিকল্পে targetAddressSpace: 'local' বা targetAddressSpace: 'loopback' নির্দিষ্ট করুন।

স্পষ্টভাবে targetAddressSpace সেট করলে দুটি গুরুত্বপূর্ণ কাজ হয়:

  1. মিক্সড কন্টেন্ট সংক্রান্ত ছাড়: DNS রেজোলিউশনের আগে মিক্সড কন্টেন্ট চেক করা হয়। targetAddressSpace: 'local' (বা 'loopback') ঘোষণা করলে, DNS রেজোলিউশনের আগেই ব্রাউজার জানতে পারে যে http:// URL (কাস্টম DNS হোস্টনেম সহ যা লোকাল IP-তে রেজোলিউট করে) লোকাল/লুপব্যাক এন্ডপয়েন্টকে টার্গেট করে এবং ব্যবহারকারী অনুমতি দিলে মিক্সড কন্টেন্ট ব্লকিং থেকে ছাড় পাবে।
  2. অ্যাড্রেস স্পেস যাচাইকরণ: DNS রেজোলিউশনের পরে, ব্রাউজার যাচাই করে যে রেজোলিউট করা IP অ্যাড্রেসটি আসলে ঘোষিত targetAddressSpace-এর অন্তর্গত। targetAddressSpace: 'local' সহ কোনও হোস্টনেম অপ্রত্যাশিতভাবে public IP অ্যাড্রেসে রিজলভ হলে, ব্রাউজার অবিলম্বে 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 থ্রো করে। পরিবর্তে, ব্রাউজার DNS রেজোলিউশনের আগে WebSocket URL পরীক্ষা করে: ws:// https:// পৃষ্ঠা থেকে শুরু করা কানেকশন মিক্সড কন্টেন্ট ব্লকিং থেকে অব্যাহতি পায় যখন URL হোস্ট সিনট্যাক্টিক্যালি লোকাল বা লুপব্যাক হিসেবে শনাক্ত করা যায়—বিশেষ করে একটি ব্যক্তিগত বা লুপব্যাক IP লিটারেল (ws://192.168.1.100:8080, ws://127.0.0.1:8080, ws://[::1]:8080), localhost (*.localhost), অথবা একটি .local mDNS হোস্টনেম (ws://device.local:8080)—এবং local-network বা loopback-network অনুমতি প্রম্পটের পিছনে গেট করা থাকে। কাস্টম নন-.local ডিএনএস হোস্টনেমের মাধ্যমে কোনও লোকাল এন্ডপয়েন্ট অ্যাড্রেস করা হলে, wss://-এর মাধ্যমে কানেক্ট করুন।
  • WebTransport: যেহেতু WebTransport-এর জন্য https:// (TLS সহ QUIC-এর মাধ্যমে HTTP/3 অথবা লোকাল সেল্ফ-সাইনড সার্টিফিকেটের জন্য serverCertificateHashes) কঠোরভাবে প্রয়োজন, তাই এটি কখনও মিক্সড কন্টেন্ট ব্লক ট্রিগার করে না এবং targetAddressSpace বিকল্প গ্রহণ করে না। পরিবর্তে, ব্রাউজার অটোমেটিক সমাধান করা IP অ্যাড্রেস স্পেস যাচাই করে এবং local-network বা loopback-network অনুমতি প্রম্পটের পিছনে transport.ready প্রতিশ্রুতি গেট করে।
/**
 * 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;
}

৪. Permissions-Policy-এর মাধ্যমে ক্রস-অরিজিন iframe-এ অ্যাক্সেস ডেলিগেট করা

ডিফল্ট হিসেবে, local-network ও loopback-network-এর কাছে 'self'-এর ডিফল্ট সাদাতালিকা থাকে, যা ক্রস-অরিজিন <iframe> এম্বেডকে লোকাল বা লুপব্যাক অনুরোধ করা থেকে ব্লক করে। এম্বেড করা ক্রস-অরিজিন <iframe>-কে (যেমন, হার্ডওয়্যার সেট-আপ উইজেট বা SSO ফ্রেম যা স্থানীয় এজেন্টের সাথে যোগাযোগ করে) স্থানীয় বা লুপব্যাক অনুরোধ শুরু করার অনুমতি দিতে, allow অ্যাট্রিবিউট এবং/অথবা HTTP Permissions-Policy হেডারে গ্র্যানুলার অনুমতি স্পষ্টভাবে ডেলিগেট করুন:

<!-- 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. অনুমতি সংক্রান্ত কোয়েরি ফলব্যাক: navigator.permissions.query({ name: permissionName })-কে try / catch-এ র‍্যাপ করুন। 'local-network' বা 'loopback-network' একটি অপরিচিত PermissionName হওয়ার কারণে ব্রাউজার যদি TypeError দেখায়, তাহলে স্ট্যাটাসকে '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';
  }
}