95 lines
7.2 KiB
JavaScript
95 lines
7.2 KiB
JavaScript
import { completeIce, rtcConfiguration, sendFrame } from './mobile/transport.mjs'
|
||
import { drawQr, InviteScanner } from './mobile/qr.mjs'
|
||
import { createDeviceIdentity, loadDeviceIdentity, assertRemoteIdentity, parsePhoneIdentity } from './mobile/peer-identity.mjs'
|
||
const host = window.mobileHost, $ = id => document.getElementById(id), peers = new Map()
|
||
const status = text => { $('status').textContent = text }
|
||
function action(id, work) { $(id).onclick = async () => { $(id).disabled = true; try { await work() } catch (error) { status(error.message) } finally { $(id).disabled = false } } }
|
||
const identityScanner = new InviteScanner(document.createElement('video'), async raw => {
|
||
if (raw.length > 700) throw new Error('Telefon kimlik belgesi çok büyük.')
|
||
const value = JSON.parse(raw)
|
||
const pairingId = $('pairing').value.trim() || value.pairingId
|
||
parsePhoneIdentity(raw, pairingId)
|
||
if (!/^[0-9a-f-]{36}$/.test(pairingId)) throw new Error('Eşleştirme kimliği geçersiz.')
|
||
$('pairing').value = pairingId; $('phone-identity').value = raw
|
||
status('Telefon kimliği QR resminden alındı. Telefondaki kısa kodu girin ve bu telefonu ayrıca onaylayın; henüz izin verilmedi.')
|
||
})
|
||
$('phone-identity-file').onchange = async event => {
|
||
try { await identityScanner.image(event.target.files[0]) }
|
||
catch (error) { status(error.message) }
|
||
finally { event.target.value = '' }
|
||
}
|
||
action('login-start', async () => { const result = await host.call('account.login.start'); $('login-code').textContent = `Açılan hesap sayfasında yalnız bu kodu onaylayın: ${result.code}` })
|
||
action('login-check', async () => { const result = await host.call('account.login.check'); status(result.approved ? 'Hesaba giriş yapıldı. Bu bilgisayarı eşleştirebilirsiniz.' : 'Tarayıcıdaki kod onayı bekleniyor.') })
|
||
action('logout', async () => { await host.call('account.logout'); listening = false; $('login-code').textContent = ''; status('Mobil oturum kapatıldı; hesap ve Google anahtarı bu bilgisayar kasasından temizlendi.') })
|
||
async function desktopIdentity(create = false) {
|
||
const setup = await host.call('setup')
|
||
if (!setup.deviceId) throw new Error('Önce bu bilgisayarı hesapla eşleştirin.')
|
||
if (setup.localFingerprint) return loadDeviceIdentity(setup.deviceId, setup.localFingerprint)
|
||
if (!create) throw new Error('Önce bu bilgisayarda yeni telefon QR kodu oluşturun.')
|
||
const identity = await createDeviceIdentity(setup.deviceId)
|
||
await host.call('identity.save', { deviceId: setup.deviceId, fingerprint: identity.fingerprint })
|
||
return identity
|
||
}
|
||
action('invite', async () => {
|
||
const identity = await desktopIdentity(true), invite = await host.call('invite.create')
|
||
drawQr($('invite-qr'), JSON.stringify({ ...invite, version: 2, desktopFingerprint: identity.fingerprint }))
|
||
$('invite-qr').hidden = false
|
||
$('invite-status').textContent = `Telefonda aynı hesaba giriş yapıp bu QR kodunu tarayın. Son geçerlilik: ${invite.expiresAt}`
|
||
})
|
||
action('invite-check', async () => { const result = await host.call('invite.check'); if (!result.pending) { status('QR ile gelen telefon henüz yok.'); return }; $('pairing').value = result.pending.pairingId; status(`${result.pending.name} bulundu. Telefondaki kısa kodu girip “Bu telefonu onayla” seçin.`) })
|
||
action('save', async () => { const token = $('account').value; $('account').value = ''; await host.call('account.save', { token }); status('Hesap anahtarı Windows kasasına kaydedildi.') })
|
||
action('pair', async () => { const pair = await host.call('desktop.pair'); $('code').textContent = `Açılan hesap sayfasında bu kodu onaylayın: ${pair.code}. Süre: ${pair.expiresAt}` })
|
||
action('approve', async () => { await host.call('phone.approve', { pairingId: $('pairing').value.trim(), code: $('phone-code').value.trim().toUpperCase(), identity: $('phone-identity').value.trim() }); $('phone-code').value = ''; $('phone-identity').value = ''; status('Telefon bu bilgisayarda kimliğiyle onaylandı.') })
|
||
action('refresh', async () => {
|
||
const data = await host.call('devices'); $('devices').replaceChildren()
|
||
for (const device of data.devices) {
|
||
const row = document.createElement('p'), label = document.createElement('span'), button = document.createElement('button')
|
||
label.textContent = `${device.name} · ${device.platform}${device.self ? ' · Bu bilgisayar' : ''}`
|
||
button.textContent = 'Cihazı iptal et'
|
||
button.onclick = async () => { button.disabled = true; try { await host.call('device.revoke', { deviceId: device.id, version: device.version }); row.remove(); status('Cihaz iptal edildi.') } catch (error) { status(error.message); button.disabled = false } }
|
||
row.append(label, button); $('devices').append(row)
|
||
}
|
||
})
|
||
function close(id) { const entry = peers.get(id); if (!entry) return; peers.delete(id); clearInterval(entry.timer); entry.pc.close(); void host.call('channel.close', { channelId: id }) }
|
||
host.onEvent(event => { if (event.close) { close(event.channelId); return }; const entry = peers.get(event.channelId); if (entry?.channel?.readyState === 'open') sendFrame(entry.channel, event.frame) })
|
||
async function connect(offer) {
|
||
assertRemoteIdentity(offer.sdp, offer.trustedPhoneFingerprint)
|
||
const ice = await host.call('ice')
|
||
const identity = await desktopIdentity()
|
||
const pc = new RTCPeerConnection({ ...rtcConfiguration(ice.iceServers), certificates: [identity.certificate] }), entry = { pc }; peers.set(offer.id, entry)
|
||
pc.ondatachannel = event => {
|
||
const channel = event.channel; entry.channel = channel
|
||
if (channel.label !== 'aiturk-mobile-v1') { close(offer.id); return }
|
||
let pending = 0
|
||
channel.onmessage = async event => {
|
||
if (typeof event.data !== 'string' || event.data.length > 65000 || ++pending > 8) { close(offer.id); return }
|
||
let message
|
||
try {
|
||
message = JSON.parse(event.data)
|
||
const result = await host.call('channel.request', { channelId: offer.id, message })
|
||
if (channel.readyState === 'open') sendFrame(channel, { id: message.id, result })
|
||
} catch (error) { if (message?.id && channel.readyState === 'open') channel.send(JSON.stringify({ id: message.id, error: error.message })) }
|
||
finally { pending-- }
|
||
}
|
||
channel.onclose = () => close(offer.id)
|
||
channel.onopen = () => status('Telefon şifreli veri kanalıyla bağlı.')
|
||
}
|
||
try {
|
||
await pc.setRemoteDescription({ type: 'offer', sdp: offer.sdp })
|
||
await pc.setLocalDescription(await pc.createAnswer()); await completeIce(pc)
|
||
await host.call('channel.answer', { channelId: offer.id, sdp: pc.localDescription.sdp })
|
||
entry.timer = setInterval(() => { void host.call('channel.heartbeat', { channelId: offer.id }).catch(() => close(offer.id)) }, 10000)
|
||
} catch (error) { close(offer.id); throw error }
|
||
}
|
||
let listening = false
|
||
action('listen', async () => {
|
||
if (listening) return
|
||
listening = true; status('Onaylı telefon bağlantısı bekleniyor.')
|
||
while (listening) {
|
||
try { const data = await host.call('inbox'); for (const offer of data.channels) if (!peers.has(offer.id)) await connect(offer) }
|
||
catch (error) { status(error.message); listening = false; break }
|
||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||
}
|
||
})
|
||
window.addEventListener('pagehide', () => { listening = false; for (const id of peers.keys()) close(id) })
|