修复wss连接、一些未知错误

This commit is contained in:
2026-03-28 18:58:52 +08:00
parent b66ba41431
commit 3d391415c6
8 changed files with 179 additions and 97 deletions

View File

@@ -5,9 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@@ -68,17 +66,10 @@ type realtimeBackplane interface {
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
origin := strings.TrimSpace(r.Header.Get("Origin")) // Device sessions are authenticated before upgrade. Keeping origin
if origin == "" { // permissive avoids false negatives behind reverse proxies or custom
// domains where Host/Forwarded headers are rewritten.
return true return true
}
parsed, err := url.Parse(origin)
if err != nil {
return false
}
return originHostMatchesRequest(parsed.Host, r)
}, },
} }
@@ -307,62 +298,3 @@ func (c *Client) writePump() {
} }
} }
} }
func originHostMatchesRequest(originHost string, r *http.Request) bool {
requestHosts := []string{
strings.TrimSpace(r.Host),
strings.TrimSpace(r.Header.Get("X-Forwarded-Host")),
}
originName, err := normalizeHost(originHost)
if err != nil {
return false
}
for _, host := range requestHosts {
if host == "" {
continue
}
requestName, err := normalizeHost(host)
if err != nil {
continue
}
if requestName == originName {
return true
}
}
return false
}
func normalizeHost(host string) (string, error) {
host = strings.TrimSpace(host)
if host == "" {
return "", fmt.Errorf("empty host")
}
if strings.Contains(host, "://") {
parsed, err := url.Parse(host)
if err != nil {
return "", err
}
host = parsed.Host
}
name, _, err := net.SplitHostPort(host)
if err == nil {
host = name
} else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
}
host = strings.ToLower(strings.TrimSpace(host))
switch host {
case "127.0.0.1", "::1":
return "localhost", nil
default:
return host, nil
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

17
frontend/dist/assets/index-DPzeYqvr.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AirShare Pro</title> <title>AirShare Pro</title>
<script type="module" crossorigin src="/assets/index-DE3lDjdM.js"></script> <script type="module" crossorigin src="/assets/index-DPzeYqvr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQ9sinZs.css"> <link rel="stylesheet" crossorigin href="/assets/index-C-7tVt-S.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -35,6 +35,7 @@ const connectedPeer = ref({
name: '--', name: '--',
type: '等待连接', type: '等待连接',
deviceId: '', deviceId: '',
networkGroupKey: '',
}) })
const transferItems = ref([]) const transferItems = ref([])
const relayServer = ref('/ws') const relayServer = ref('/ws')
@@ -82,6 +83,17 @@ const pendingTransferItems = computed(() =>
) )
const hasPendingItems = computed(() => pendingTransferItems.value.length > 0) const hasPendingItems = computed(() => pendingTransferItems.value.length > 0)
const currentTransferNetworkHint = computed(() => {
if (!connectedPeer.value.deviceId) {
return ''
}
if (isSameLocalNetwork(connectedPeer.value.networkGroupKey) || hasTurnRelayConfigured()) {
return ''
}
return '当前是跨网络访问,未配置 TURN 时实时通道可能失败。文本和小文件可回退中转,大文件建议使用 MinIO。'
})
watch( watch(
theme, theme,
@@ -208,7 +220,7 @@ async function registerCurrentDevice() {
device_id: deviceId, device_id: deviceId,
name: deviceName, name: deviceName,
type: deviceType, type: deviceType,
network_group_key: window.location.hostname || 'local', network_group_key: deriveNetworkGroupKey(),
}) })
localStorage.setItem(DEVICE_ID_KEY, device.id) localStorage.setItem(DEVICE_ID_KEY, device.id)
@@ -269,7 +281,7 @@ async function refreshCandidates() {
description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`, description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`,
icon: mapDeviceIcon(device.type), icon: mapDeviceIcon(device.type),
connectionType: connectionType:
device.network_group_key && device.network_group_key === window.location.hostname isSameLocalNetwork(device.network_group_key)
? '局域网直连优先' ? '局域网直连优先'
: '点对点传输', : '点对点传输',
})) }))
@@ -412,6 +424,7 @@ function connectToPeer(device) {
name: device.name, name: device.name,
type: device.connectionType || device.type || '点对点传输', type: device.connectionType || device.type || '点对点传输',
deviceId: device.deviceId || device.id || '', deviceId: device.deviceId || device.id || '',
networkGroupKey: device.network_group_key || '',
} }
isWaitingRoom.value = false isWaitingRoom.value = false
generatedRoomCode.value = '----' generatedRoomCode.value = '----'
@@ -439,6 +452,7 @@ function ensurePeerSession(device, preserveItems = false) {
name: device.name, name: device.name,
type: device.connectionType || device.type || '点对点传输', type: device.connectionType || device.type || '点对点传输',
deviceId: nextDeviceId, deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
} }
isWaitingRoom.value = false isWaitingRoom.value = false
generatedRoomCode.value = '----' generatedRoomCode.value = '----'
@@ -457,6 +471,7 @@ function closeTransfer() {
name: '--', name: '--',
type: '等待连接', type: '等待连接',
deviceId: '', deviceId: '',
networkGroupKey: '',
} }
viewMode.value = 'main' viewMode.value = 'main'
connectedPeer.value.baseType = '等待连接' connectedPeer.value.baseType = '等待连接'
@@ -733,7 +748,7 @@ async function copyTextItem(id) {
} }
try { try {
await navigator.clipboard.writeText(item.text) await copyToClipboard(item.text)
item.copied = true item.copied = true
window.setTimeout(() => { window.setTimeout(() => {
@@ -748,6 +763,33 @@ async function copyTextItem(id) {
} }
} }
async function copyToClipboard(value) {
if (navigator.clipboard?.writeText && window.isSecureContext) {
await navigator.clipboard.writeText(value)
return
}
const textarea = document.createElement('textarea')
textarea.value = value
textarea.setAttribute('readonly', 'readonly')
textarea.style.position = 'fixed'
textarea.style.top = '0'
textarea.style.left = '-9999px'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
try {
if (!document.execCommand('copy')) {
throw new Error('copy command failed')
}
} finally {
document.body.removeChild(textarea)
}
}
function simulateUpload(id) { function simulateUpload(id) {
clearUploadTimer(id) clearUploadTimer(id)
@@ -1169,6 +1211,48 @@ function buildRelayServerPath() {
return `${protocol}//${window.location.host}/ws` return `${protocol}//${window.location.host}/ws`
} }
function hasTurnRelayConfigured() {
return Array.isArray(runtimeConfig.value?.turn_urls) && runtimeConfig.value.turn_urls.some((item) => String(item || '').trim())
}
function deriveNetworkGroupKey() {
const hostname = String(window.location.hostname || '').trim().toLowerCase()
if (!hostname) {
return 'local'
}
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname.endsWith('.local')) {
return hostname
}
if (isPrivateIPv4Host(hostname)) {
return hostname
}
return ''
}
function isSameLocalNetwork(networkGroupKey) {
const localNetworkGroupKey = deriveNetworkGroupKey()
return !!localNetworkGroupKey && networkGroupKey === localNetworkGroupKey
}
function isPrivateIPv4Host(hostname) {
const parts = hostname.split('.')
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) {
return false
}
const [first, second] = parts.map((part) => Number(part))
if (first === 10 || first === 127) {
return true
}
if (first === 192 && second === 168) {
return true
}
return first === 172 && second >= 16 && second <= 31
}
function buildIceServers(configValue) { function buildIceServers(configValue) {
const urls = Array.isArray(configValue?.turn_urls) const urls = Array.isArray(configValue?.turn_urls)
? configValue.turn_urls.map((item) => String(item || '').trim()).filter(Boolean) ? configValue.turn_urls.map((item) => String(item || '').trim()).filter(Boolean)
@@ -1235,12 +1319,32 @@ function syncConnectionTypeLabel(status = '') {
} }
} }
function shouldRecreateRealtimeTransport(targetDeviceId) {
if (!peerConnection || currentRtcPeerId !== targetDeviceId) {
return true
}
if (peerConnection.signalingState === 'closed') {
return true
}
if (['failed', 'disconnected', 'closed'].includes(peerConnection.connectionState)) {
return true
}
if (['failed', 'disconnected', 'closed'].includes(peerConnection.iceConnectionState)) {
return true
}
return !controlChannel || controlChannel.readyState === 'closed'
}
async function ensureRealtimeTransport(targetDeviceId, options = {}) { async function ensureRealtimeTransport(targetDeviceId, options = {}) {
if (!targetDeviceId || !supportsWebRTC()) { if (!targetDeviceId || !supportsWebRTC()) {
return null return null
} }
if (!peerConnection || currentRtcPeerId !== targetDeviceId) { if (shouldRecreateRealtimeTransport(targetDeviceId)) {
teardownRealtimeTransport() teardownRealtimeTransport()
createPeerConnection(targetDeviceId) createPeerConnection(targetDeviceId)
} }
@@ -1582,6 +1686,7 @@ function buildPeerSessionFromDevice(deviceId, fallbackConnectionType = '等待
name: knownDevice?.name || `设备 ${shortId(deviceId)}`, name: knownDevice?.name || `设备 ${shortId(deviceId)}`,
type: formatDeviceType(knownDevice?.type || 'desktop'), type: formatDeviceType(knownDevice?.type || 'desktop'),
connectionType: fallbackConnectionType, connectionType: fallbackConnectionType,
network_group_key: knownDevice?.network_group_key || '',
} }
} }
@@ -1797,12 +1902,35 @@ function handleRelayMessage(raw) {
if (envelope.type === 'transfer.file') { if (envelope.type === 'transfer.file') {
handleIncomingTransferFile(envelope) handleIncomingTransferFile(envelope)
return
}
if (envelope.type === 'peer.session.closed') {
handlePeerSessionClosed(envelope)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} }
function handlePeerSessionClosed(envelope) {
const senderDeviceId = envelope.device_id || ''
if (!senderDeviceId || connectedPeer.value.deviceId !== senderDeviceId) {
return
}
teardownRealtimeTransport()
resetTransferItems()
connectedPeer.value = {
name: '--',
type: '绛夊緟杩炴帴',
baseType: '绛夊緟杩炴帴',
deviceId: '',
networkGroupKey: '',
}
viewMode.value = 'main'
}
function handleIncomingTransferCreated(envelope) { function handleIncomingTransferCreated(envelope) {
const payload = envelope.payload || {} const payload = envelope.payload || {}
const senderDeviceId = envelope.device_id || payload.sender_device_id || '' const senderDeviceId = envelope.device_id || payload.sender_device_id || ''
@@ -2178,7 +2306,7 @@ refreshCandidates = async function refreshCandidatesOverride() {
description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`, description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`,
icon: mapDeviceIcon(device.type), icon: mapDeviceIcon(device.type),
connectionType: connectionType:
device.network_group_key && device.network_group_key === window.location.hostname isSameLocalNetwork(device.network_group_key)
? '局域网直连优先' ? '局域网直连优先'
: '跨网络实时传输', : '跨网络实时传输',
})) }))
@@ -2206,6 +2334,7 @@ connectToPeer = function connectToPeerOverride(device) {
type: nextBaseType, type: nextBaseType,
baseType: nextBaseType, baseType: nextBaseType,
deviceId: nextDeviceId, deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
} }
isWaitingRoom.value = false isWaitingRoom.value = false
generatedRoomCode.value = '----' generatedRoomCode.value = '----'
@@ -2231,6 +2360,7 @@ ensurePeerSession = function ensurePeerSessionOverride(device, preserveItems = f
type: nextBaseType, type: nextBaseType,
baseType: nextBaseType, baseType: nextBaseType,
deviceId: nextDeviceId, deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
} }
isWaitingRoom.value = false isWaitingRoom.value = false
generatedRoomCode.value = '----' generatedRoomCode.value = '----'
@@ -2243,6 +2373,10 @@ ensurePeerSession = function ensurePeerSessionOverride(device, preserveItems = f
} }
closeTransfer = function closeTransferOverride() { closeTransfer = function closeTransferOverride() {
if (connectedPeer.value.deviceId) {
relayToPeer('peer.session.closed', connectedPeer.value.deviceId, {})
}
teardownRealtimeTransport() teardownRealtimeTransport()
resetTransferItems() resetTransferItems()
connectedPeer.value = { connectedPeer.value = {
@@ -2250,6 +2384,7 @@ closeTransfer = function closeTransferOverride() {
type: '等待连接', type: '等待连接',
baseType: '等待连接', baseType: '等待连接',
deviceId: '', deviceId: '',
networkGroupKey: '',
} }
viewMode.value = 'main' viewMode.value = 'main'
} }
@@ -2709,6 +2844,7 @@ handleIncomingTransferFile = function handleIncomingTransferFileOverride(envelop
:connection-type="connectedPeer.type" :connection-type="connectedPeer.type"
:has-pending-items="hasPendingItems" :has-pending-items="hasPendingItems"
:items="transferItems" :items="transferItems"
:network-hint="currentTransferNetworkHint"
:peer-name="connectedPeer.name" :peer-name="connectedPeer.name"
@close="closeTransfer" @close="closeTransfer"
@copy-item="copyTextItem" @copy-item="copyTextItem"

View File

@@ -12,6 +12,10 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
networkHint: {
type: String,
default: '',
},
items: { items: {
type: Array, type: Array,
required: true, required: true,
@@ -84,6 +88,7 @@ function handleDrop(event) {
<div class="connected-to"> <div class="connected-to">
<h2>{{ peerName }}</h2> <h2>{{ peerName }}</h2>
<p>{{ connectionType }}</p> <p>{{ connectionType }}</p>
<small v-if="networkHint" class="connection-hint">{{ networkHint }}</small>
</div> </div>
<button class="close-btn" type="button" @click="$emit('close')"> <button class="close-btn" type="button" @click="$emit('close')">
<LocalIcon name="close" size="20" /> <LocalIcon name="close" size="20" />

View File

@@ -708,6 +708,15 @@ input.room-code::placeholder {
color: var(--success-green); color: var(--success-green);
} }
.connection-hint {
display: block;
margin-top: 8px;
max-width: 42ch;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
}
.close-btn { .close-btn {
width: 36px; width: 36px;
height: 36px; height: 36px;