major: kestrel is now a tak server (#6)
All checks were successful
ci/woodpecker/push/push Pipeline was successful
All checks were successful
ci/woodpecker/push/push Pipeline was successful
## Added - CoT (Cursor on Target) server on port 8089 enabling ATAK/iTAK device connectivity - Support for TAK stream protocol and traditional XML CoT messages - TLS/SSL support with automatic fallback to plain TCP - Username/password authentication for CoT connections - Real-time device position tracking with TTL-based expiration (90s default) - API endpoints: `/api/cot/config`, `/api/cot/server-package`, `/api/cot/truststore`, `/api/me/cot-password` - TAK Server section in Settings with QR code for iTAK setup - ATAK password management in Account page for OIDC users - CoT device markers on map showing real-time positions - Comprehensive documentation in `docs/` directory - Environment variables: `COT_PORT`, `COT_TTL_MS`, `COT_REQUIRE_AUTH`, `COT_SSL_CERT`, `COT_SSL_KEY`, `COT_DEBUG` - Dependencies: `fast-xml-parser`, `jszip`, `qrcode` ## Changed - Authentication system supports CoT password management for OIDC users - Database schema includes `cot_password_hash` field - Test suite refactored to follow functional design principles ## Removed - Consolidated utility modules: `authConfig.js`, `authSkipPaths.js`, `bootstrap.js`, `poiConstants.js`, `session.js` ## Security - XML entity expansion protection in CoT parser - Enhanced input validation and SQL injection prevention - Authentication timeout to prevent hanging connections ## Breaking Changes - Port 8089 must be exposed for CoT server. Update firewall rules and Docker/Kubernetes configurations. ## Migration Notes - OIDC users must set ATAK password via Account settings before connecting - Docker: expose port 8089 (`-p 8089:8089`) - Kubernetes: update Helm values to expose port 8089 Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>. Use the same URL or set MEDIASOUP_ANNOUNCED_IP.
|
||||
</p>
|
||||
<ul class="normal-case list-inside list-disc text-left text-kestrel-muted">
|
||||
<li><strong>Firewall:</strong> Open UDP/TCP 40000–49999 on the server.</li>
|
||||
<li><strong>Firewall:</strong> Open UDP/TCP 40000-49999 on the server.</li>
|
||||
<li><strong>Wrong host:</strong> Server must see the same address you use.</li>
|
||||
<li><strong>Restrictive NAT / cellular:</strong> TURN may be required.</li>
|
||||
</ul>
|
||||
@@ -66,7 +66,7 @@
|
||||
Wrong host: server sees <strong>{{ failureReason.wrongHost.serverHostname }}</strong> but you opened at <strong>{{ failureReason.wrongHost.clientHostname }}</strong>.
|
||||
</p>
|
||||
<ul class="normal-case list-inside list-disc text-left text-kestrel-muted">
|
||||
<li>Firewall: open ports 40000–49999.</li>
|
||||
<li>Firewall: open ports 40000-49999.</li>
|
||||
<li>Wrong host: use same URL or set MEDIASOUP_ANNOUNCED_IP.</li>
|
||||
<li>Restrictive NAT: TURN may be required.</li>
|
||||
</ul>
|
||||
@@ -104,9 +104,9 @@ const hasStream = ref(false)
|
||||
const error = ref('')
|
||||
const connectionState = ref('') // '', 'connecting', 'connected', 'failed'
|
||||
const failureReason = ref(null) // { wrongHost: { serverHostname, clientHostname } | null }
|
||||
let device = null
|
||||
let recvTransport = null
|
||||
let consumer = null
|
||||
const device = ref(null)
|
||||
const recvTransport = ref(null)
|
||||
const consumer = ref(null)
|
||||
|
||||
async function runFailureReasonCheck() {
|
||||
failureReason.value = await getWebRTCFailureReason()
|
||||
@@ -135,16 +135,16 @@ async function setupWebRTC() {
|
||||
const rtpCapabilities = await $fetch(`/api/live/webrtc/router-rtp-capabilities?sessionId=${props.session.id}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
device = await createMediasoupDevice(rtpCapabilities)
|
||||
recvTransport = await createRecvTransport(device, props.session.id)
|
||||
device.value = await createMediasoupDevice(rtpCapabilities)
|
||||
recvTransport.value = await createRecvTransport(device.value, props.session.id)
|
||||
|
||||
recvTransport.on('connectionstatechange', () => {
|
||||
const state = recvTransport.connectionState
|
||||
recvTransport.value.on('connectionstatechange', () => {
|
||||
const state = recvTransport.value.connectionState
|
||||
if (state === 'connected') connectionState.value = 'connected'
|
||||
else if (state === 'failed' || state === 'disconnected' || state === 'closed') {
|
||||
logWarn('LiveSessionPanel: Receive transport connection state changed', {
|
||||
state,
|
||||
transportId: recvTransport.id,
|
||||
transportId: recvTransport.value.id,
|
||||
sessionId: props.session.id,
|
||||
})
|
||||
if (state === 'failed') {
|
||||
@@ -154,8 +154,8 @@ async function setupWebRTC() {
|
||||
}
|
||||
})
|
||||
|
||||
const connectionPromise = waitForConnectionState(recvTransport, 10000)
|
||||
consumer = await consumeProducer(recvTransport, device, props.session.id)
|
||||
const connectionPromise = waitForConnectionState(recvTransport.value, 10000)
|
||||
consumer.value = await consumeProducer(recvTransport.value, device.value, props.session.id)
|
||||
const finalConnectionState = await connectionPromise
|
||||
|
||||
if (finalConnectionState !== 'connected') {
|
||||
@@ -163,8 +163,8 @@ async function setupWebRTC() {
|
||||
runFailureReasonCheck()
|
||||
logWarn('LiveSessionPanel: Transport not fully connected', {
|
||||
state: finalConnectionState,
|
||||
transportId: recvTransport.id,
|
||||
consumerId: consumer.id,
|
||||
transportId: recvTransport.value.id,
|
||||
consumerId: consumer.value.id,
|
||||
})
|
||||
}
|
||||
else {
|
||||
@@ -182,14 +182,14 @@ async function setupWebRTC() {
|
||||
attempts++
|
||||
}
|
||||
|
||||
if (!consumer.track) {
|
||||
if (!consumer.value.track) {
|
||||
logError('LiveSessionPanel: No video track available', {
|
||||
consumerId: consumer.id,
|
||||
consumerKind: consumer.kind,
|
||||
consumerPaused: consumer.paused,
|
||||
consumerClosed: consumer.closed,
|
||||
consumerProducerId: consumer.producerId,
|
||||
transportConnectionState: recvTransport?.connectionState,
|
||||
consumerId: consumer.value.id,
|
||||
consumerKind: consumer.value.kind,
|
||||
consumerPaused: consumer.value.paused,
|
||||
consumerClosed: consumer.value.closed,
|
||||
consumerProducerId: consumer.value.producerId,
|
||||
transportConnectionState: recvTransport.value?.connectionState,
|
||||
})
|
||||
error.value = 'No video track available - consumer may not be receiving data from producer'
|
||||
return
|
||||
@@ -197,14 +197,14 @@ async function setupWebRTC() {
|
||||
|
||||
if (!videoRef.value) {
|
||||
logError('LiveSessionPanel: Video ref not available', {
|
||||
consumerId: consumer.id,
|
||||
hasTrack: !!consumer.track,
|
||||
consumerId: consumer.value.id,
|
||||
hasTrack: !!consumer.value.track,
|
||||
})
|
||||
error.value = 'Video element not available'
|
||||
return
|
||||
}
|
||||
|
||||
const stream = new MediaStream([consumer.track])
|
||||
const stream = new MediaStream([consumer.value.track])
|
||||
videoRef.value.srcObject = stream
|
||||
hasStream.value = true
|
||||
|
||||
@@ -227,7 +227,7 @@ async function setupWebRTC() {
|
||||
if (resolved) return
|
||||
resolved = true
|
||||
videoRef.value.removeEventListener('loadedmetadata', handler)
|
||||
logWarn('LiveSessionPanel: Video metadata timeout', { consumerId: consumer.id })
|
||||
logWarn('LiveSessionPanel: Video metadata timeout', { consumerId: consumer.value.id })
|
||||
resolve()
|
||||
}, 5000)
|
||||
})
|
||||
@@ -239,7 +239,7 @@ async function setupWebRTC() {
|
||||
}
|
||||
catch (playErr) {
|
||||
logWarn('LiveSessionPanel: Video play() failed (may need user interaction)', {
|
||||
consumerId: consumer.id,
|
||||
consumerId: consumer.value.id,
|
||||
error: playErr.message || String(playErr),
|
||||
errorName: playErr.name,
|
||||
videoPaused: videoRef.value.paused,
|
||||
@@ -248,12 +248,12 @@ async function setupWebRTC() {
|
||||
// Don't set error - video might still work, just needs user interaction
|
||||
}
|
||||
|
||||
consumer.track.addEventListener('ended', () => {
|
||||
consumer.value.track.addEventListener('ended', () => {
|
||||
error.value = 'Video track ended'
|
||||
hasStream.value = false
|
||||
})
|
||||
videoRef.value.addEventListener('error', () => {
|
||||
logError('LiveSessionPanel: Video element error', { consumerId: consumer.id })
|
||||
logError('LiveSessionPanel: Video element error', { consumerId: consumer.value.id })
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -274,15 +274,15 @@ async function setupWebRTC() {
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (consumer) {
|
||||
consumer.close()
|
||||
consumer = null
|
||||
if (consumer.value) {
|
||||
consumer.value.close()
|
||||
consumer.value = null
|
||||
}
|
||||
if (recvTransport) {
|
||||
recvTransport.close()
|
||||
recvTransport = null
|
||||
if (recvTransport.value) {
|
||||
recvTransport.value.close()
|
||||
recvTransport.value = null
|
||||
}
|
||||
device = null
|
||||
device.value = null
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = null
|
||||
}
|
||||
@@ -308,7 +308,7 @@ watch(
|
||||
watch(
|
||||
() => props.session?.hasStream,
|
||||
(hasStream) => {
|
||||
if (hasStream && props.session?.id && !device) {
|
||||
if (hasStream && props.session?.id && !device.value) {
|
||||
setupWebRTC()
|
||||
}
|
||||
else if (!hasStream) {
|
||||
|
||||
Reference in New Issue
Block a user