fix
This commit is contained in:
@@ -235,6 +235,11 @@ URL, независимо от исходного имени файла. Лок
|
|||||||
реальные игровые материалы и p95 на M1 пока не подтверждены. Все восемь визуальных режимов доступны в SCREEN-005; авторизация и получение
|
реальные игровые материалы и p95 на M1 пока не подтверждены. Все восемь визуальных режимов доступны в SCREEN-005; авторизация и получение
|
||||||
серверных команд остаются в SCREEN-006.
|
серверных команд остаются в SCREEN-006.
|
||||||
|
|
||||||
|
API и загрузка материалов используют сетевой стек Chromium (`net.fetch` Electron)
|
||||||
|
с системными настройками прокси. Собственный прокси приложение не запускает;
|
||||||
|
воспроизведение остаётся локальным. Ошибки DNS, подключения, TLS и прокси
|
||||||
|
показываются с безопасным кодом и контекстом запроса.
|
||||||
|
|
||||||
## Пульт и режимы SCREEN-005
|
## Пульт и режимы SCREEN-005
|
||||||
|
|
||||||
Пульт включает подключение игры через сервер, план шагов с восемью режимами, текстом и параметрами таймера,
|
Пульт включает подключение игры через сервер, план шагов с восемью режимами, текстом и параметрами таймера,
|
||||||
|
|||||||
@@ -245,3 +245,10 @@ SCREEN-009 отменена владельцем; ID не переиспольз
|
|||||||
проектор/звук и физические замеры на M1. Статус IN_PROGRESS до получения
|
проектор/звук и физические замеры на M1. Статус IN_PROGRESS до получения
|
||||||
результатов всех стендов. SCREEN-001/004/007 также не закрываются.
|
результатов всех стендов. SCREEN-001/004/007 также не закрываются.
|
||||||
Бэкенд, сайт, protobuf и зависимости не менялись; commit/push не выполнялись.
|
Бэкенд, сайт, protobuf и зависимости не менялись; commit/push не выполнялись.
|
||||||
|
|
||||||
|
## SCREEN-024 — Сетевой стек Electron для API и материалов
|
||||||
|
|
||||||
|
- Статус: `IN_PROGRESS`.
|
||||||
|
- Причина: сайт авторизуется, приложение получает UND_ERR_CONNECT_TIMEOUT; API и файлы приложения используют Node fetch, обходящий браузерный сетевой стек и системный прокси Chromium.
|
||||||
|
- Изменение: внедрить net.fetch Electron в существующие Online и PackageStore без смены API/адресов/таймаутов; сохранить безопасные ошибки сети.
|
||||||
|
- Проверка: тесты, type-check, lint, lint:css, build, Electron smoke с HTTP-прокси и неразрешимым доменом.
|
||||||
|
|||||||
+7
-2
@@ -3,6 +3,7 @@ import {
|
|||||||
dialog,
|
dialog,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
|
net,
|
||||||
protocol,
|
protocol,
|
||||||
screen,
|
screen,
|
||||||
session,
|
session,
|
||||||
@@ -289,7 +290,11 @@ app
|
|||||||
diagnostics.record('settings-error')
|
diagnostics.record('settings-error')
|
||||||
hostNotice = error instanceof Error ? error.message : 'Не удалось восстановить окна'
|
hostNotice = error instanceof Error ? error.message : 'Не удалось восстановить окна'
|
||||||
}
|
}
|
||||||
const packages = new PackageStore(join(app.getPath('userData'), 'packages'))
|
const fetchNetwork: typeof fetch = (input, init) =>
|
||||||
|
net.fetch(input instanceof URL ? input.href : input, init)
|
||||||
|
const packages = new PackageStore(join(app.getPath('userData'), 'packages'), {
|
||||||
|
fetch: fetchNetwork,
|
||||||
|
})
|
||||||
await packages.initialize()
|
await packages.initialize()
|
||||||
online = new Online(
|
online = new Online(
|
||||||
join(app.getPath('userData'), 'connection'),
|
join(app.getPath('userData'), 'connection'),
|
||||||
@@ -303,7 +308,7 @@ app
|
|||||||
encrypt: (value) => safeStorage.encryptString(value),
|
encrypt: (value) => safeStorage.encryptString(value),
|
||||||
decrypt: (value) => safeStorage.decryptString(value),
|
decrypt: (value) => safeStorage.decryptString(value),
|
||||||
},
|
},
|
||||||
undefined,
|
fetchNetwork,
|
||||||
(failure) => diagnostics?.record('request-error', failure),
|
(failure) => diagnostics?.record('request-error', failure),
|
||||||
async () => {
|
async () => {
|
||||||
const result = await dialog.showOpenDialog({
|
const result = await dialog.showOpenDialog({
|
||||||
|
|||||||
@@ -33,3 +33,35 @@ test('контекст HTTP сохраняет тип ошибки, статус
|
|||||||
result.message,
|
result.message,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ошибки Chromium сохраняют безопасный код без адресов и содержимого', () => {
|
||||||
|
for (const [input, code, message] of [
|
||||||
|
['ERR_NAME_NOT_RESOLVED', 'ENOTFOUND', 'Не удалось определить адрес сервера'],
|
||||||
|
['ERR_CONNECTION_REFUSED', 'ECONNREFUSED', 'Сервер отклонил соединение'],
|
||||||
|
['ERR_CONNECTION_TIMED_OUT', 'ETIMEDOUT', 'Превышено время ожидания соединения с сервером'],
|
||||||
|
[
|
||||||
|
'ERR_PROXY_CONNECTION_FAILED',
|
||||||
|
'ERR_PROXY_CONNECTION_FAILED',
|
||||||
|
'Не удалось подключиться через системный прокси',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ERR_TUNNEL_CONNECTION_FAILED',
|
||||||
|
'ERR_TUNNEL_CONNECTION_FAILED',
|
||||||
|
'Не удалось подключиться через системный прокси',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'ERR_CERT_AUTHORITY_INVALID',
|
||||||
|
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
||||||
|
'Ошибка защищённого соединения с сервером',
|
||||||
|
],
|
||||||
|
['ERR_UNKNOWN', 'NETWORK', 'Сервер недоступен'],
|
||||||
|
]) {
|
||||||
|
const result = networkFailure(
|
||||||
|
new Error(`net::${input} at https://SECRET.invalid`),
|
||||||
|
new AbortController().signal,
|
||||||
|
)
|
||||||
|
assert.equal(result.code, code)
|
||||||
|
assert.equal(result.message, message)
|
||||||
|
assert.ok(!result.message.includes('SECRET'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const NETWORK_ERROR_CODES = [
|
export const NETWORK_ERROR_CODES = [
|
||||||
'NETWORK',
|
'NETWORK',
|
||||||
|
'ERR_PROXY_CONNECTION_FAILED',
|
||||||
|
'ERR_TUNNEL_CONNECTION_FAILED',
|
||||||
'TIMEOUT',
|
'TIMEOUT',
|
||||||
'ABORTED',
|
'ABORTED',
|
||||||
'ENOTFOUND',
|
'ENOTFOUND',
|
||||||
@@ -41,8 +43,26 @@ export class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chromiumCodes: Record<string, NetworkErrorCode> = {
|
||||||
|
ERR_NAME_NOT_RESOLVED: 'ENOTFOUND',
|
||||||
|
ERR_CONNECTION_REFUSED: 'ECONNREFUSED',
|
||||||
|
ERR_CONNECTION_RESET: 'ECONNRESET',
|
||||||
|
ERR_CONNECTION_TIMED_OUT: 'ETIMEDOUT',
|
||||||
|
ERR_TIMED_OUT: 'ETIMEDOUT',
|
||||||
|
ERR_PROXY_CONNECTION_FAILED: 'ERR_PROXY_CONNECTION_FAILED',
|
||||||
|
ERR_TUNNEL_CONNECTION_FAILED: 'ERR_TUNNEL_CONNECTION_FAILED',
|
||||||
|
ERR_CERT_DATE_INVALID: 'CERT_HAS_EXPIRED',
|
||||||
|
ERR_CERT_AUTHORITY_INVALID: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
||||||
|
ERR_CERT_COMMON_NAME_INVALID: 'ERR_TLS_CERT_ALTNAME_INVALID',
|
||||||
|
}
|
||||||
|
|
||||||
function causeCode(error: unknown, depth = 0): NetworkErrorCode | undefined {
|
function causeCode(error: unknown, depth = 0): NetworkErrorCode | undefined {
|
||||||
if (!error || typeof error !== 'object' || depth > 3) return
|
if (!error || typeof error !== 'object' || depth > 3) return
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const match = /^net::(ERR_[A-Z_]+)(?:\s|$)/.exec(error.message)
|
||||||
|
const code = match ? chromiumCodes[match[1]!] : undefined
|
||||||
|
if (code) return code
|
||||||
|
}
|
||||||
const value = error as { code?: unknown; cause?: unknown; errors?: unknown[] }
|
const value = error as { code?: unknown; cause?: unknown; errors?: unknown[] }
|
||||||
if (NETWORK_ERROR_CODES.includes(value.code as NetworkErrorCode))
|
if (NETWORK_ERROR_CODES.includes(value.code as NetworkErrorCode))
|
||||||
return value.code as NetworkErrorCode
|
return value.code as NetworkErrorCode
|
||||||
@@ -64,6 +84,8 @@ export function networkFailure(error: unknown, signal: AbortSignal): NetworkErro
|
|||||||
const message =
|
const message =
|
||||||
code === 'ENOTFOUND' || code === 'EAI_AGAIN'
|
code === 'ENOTFOUND' || code === 'EAI_AGAIN'
|
||||||
? 'Не удалось определить адрес сервера'
|
? 'Не удалось определить адрес сервера'
|
||||||
|
: code === 'ERR_PROXY_CONNECTION_FAILED' || code === 'ERR_TUNNEL_CONNECTION_FAILED'
|
||||||
|
? 'Не удалось подключиться через системный прокси'
|
||||||
: code.includes('CERT') || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
|
: code.includes('CERT') || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
|
||||||
? 'Ошибка защищённого соединения с сервером'
|
? 'Ошибка защищённого соединения с сервером'
|
||||||
: code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT'
|
: code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const assert = require('node:assert/strict')
|
|||||||
const { createServer } = require('node:http')
|
const { createServer } = require('node:http')
|
||||||
const { readFileSync, writeFileSync, mkdtempSync, rmSync } = require('node:fs')
|
const { readFileSync, writeFileSync, mkdtempSync, rmSync } = require('node:fs')
|
||||||
const { tmpdir } = require('node:os')
|
const { tmpdir } = require('node:os')
|
||||||
const { dialog } = require('electron')
|
const { dialog, session } = require('electron')
|
||||||
const { join } = require('node:path')
|
const { join } = require('node:path')
|
||||||
|
|
||||||
module.exports = async function onlineSmoke({
|
module.exports = async function onlineSmoke({
|
||||||
@@ -49,7 +49,11 @@ module.exports = async function onlineSmoke({
|
|||||||
const chunks = []
|
const chunks = []
|
||||||
for await (const chunk of req) chunks.push(chunk)
|
for await (const chunk of req) chunks.push(chunk)
|
||||||
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {}
|
||||||
const path = req.url
|
assert.ok(
|
||||||
|
req.url.startsWith('http://screen-proxy.invalid/'),
|
||||||
|
'API and media use Chromium proxy',
|
||||||
|
)
|
||||||
|
const path = new URL(req.url).pathname
|
||||||
if (offline && path !== '/api/screens/state') {
|
if (offline && path !== '/api/screens/state') {
|
||||||
res.destroy()
|
res.destroy()
|
||||||
return
|
return
|
||||||
@@ -168,8 +172,17 @@ module.exports = async function onlineSmoke({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
['/api/files/intro.wav', '/api/files/music-a.wav', '/api/files/music-b.wav'].includes(path)
|
[
|
||||||
|
'/api/files/intro.wav',
|
||||||
|
'/api/files/cached-intro.wav',
|
||||||
|
'/api/files/music-a.wav',
|
||||||
|
'/api/files/music-b.wav',
|
||||||
|
].includes(path)
|
||||||
) {
|
) {
|
||||||
|
if (path === '/api/files/intro.wav') {
|
||||||
|
res.writeHead(302, { Location: '/api/files/cached-intro.wav' })
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
if (path.endsWith('music-b.wav') && holdTrack) {
|
if (path.endsWith('music-b.wav') && holdTrack) {
|
||||||
trackRequested = true
|
trackRequested = true
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -224,7 +237,13 @@ module.exports = async function onlineSmoke({
|
|||||||
send({}, 404)
|
send({}, 404)
|
||||||
})
|
})
|
||||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||||
const url = `http://127.0.0.1:${server.address().port}`
|
const url = 'http://screen-proxy.invalid'
|
||||||
|
await session.defaultSession.setProxy({
|
||||||
|
mode: 'fixed_servers',
|
||||||
|
proxyRules: `http=127.0.0.1:${server.address().port}`,
|
||||||
|
proxyBypassRules: '<-loopback>',
|
||||||
|
})
|
||||||
|
await session.defaultSession.closeAllConnections()
|
||||||
const act = (intent) => evaluate(`window.screenBridge.onlineAction(${JSON.stringify(intent)})`)
|
const act = (intent) => evaluate(`window.screenBridge.onlineAction(${JSON.stringify(intent)})`)
|
||||||
try {
|
try {
|
||||||
await evaluate('window.screenBridge.closeOutput()')
|
await evaluate('window.screenBridge.closeOutput()')
|
||||||
@@ -832,8 +851,13 @@ module.exports = async function onlineSmoke({
|
|||||||
),
|
),
|
||||||
'current local game stays visible after logout',
|
'current local game stays visible after logout',
|
||||||
)
|
)
|
||||||
|
assert.ok(requests.includes('/api/auth/login'), 'login through proxy')
|
||||||
|
assert.ok(requests.includes('/api/files/cached-intro.wav'), 'redirected media through proxy')
|
||||||
|
console.log('SCREEN_PROXY_SMOKE_PASS')
|
||||||
console.log('SCREEN_ONLINE_SMOKE_PASS', JSON.stringify({ polls, requests: requests.length }))
|
console.log('SCREEN_ONLINE_SMOKE_PASS', JSON.stringify({ polls, requests: requests.length }))
|
||||||
} finally {
|
} finally {
|
||||||
|
await session.defaultSession.setProxy({ mode: 'system' })
|
||||||
|
await session.defaultSession.closeAllConnections()
|
||||||
server.closeAllConnections()
|
server.closeAllConnections()
|
||||||
await new Promise((resolve) => server.close(resolve))
|
await new Promise((resolve) => server.close(resolve))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user