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:
@@ -39,7 +39,7 @@
|
||||
Wrong host: server sees <strong>{{ webrtcFailureReason.wrongHost.serverHostname }}</strong> but you opened this page at <strong>{{ webrtcFailureReason.wrongHost.clientHostname }}</strong>. Use the same URL on phone and server, or set MEDIASOUP_ANNOUNCED_IP.
|
||||
</p>
|
||||
<ul class="mt-2 list-inside list-disc space-y-0.5 text-kestrel-muted">
|
||||
<li><strong>Firewall:</strong> Open UDP/TCP ports 40000–49999 on the server.</li>
|
||||
<li><strong>Firewall:</strong> Open UDP/TCP ports 40000-49999 on the server.</li>
|
||||
<li><strong>Wrong host:</strong> Server must see the same address you use (see above or open /api/live/debug-request-host).</li>
|
||||
<li><strong>Restrictive NAT / cellular:</strong> A TURN server may be required (future enhancement).</li>
|
||||
</ul>
|
||||
@@ -68,7 +68,7 @@
|
||||
v-if="sharing"
|
||||
class="absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs text-green-400"
|
||||
>
|
||||
● Live — you appear on the map
|
||||
● Live - you appear on the map
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,11 +122,11 @@ const starting = ref(false)
|
||||
const isSecureContext = typeof window !== 'undefined' && window.isSecureContext
|
||||
const webrtcState = ref('') // '', 'connecting', 'connected', 'failed'
|
||||
const webrtcFailureReason = ref(null) // { wrongHost: { serverHostname, clientHostname } | null }
|
||||
let locationWatchId = null
|
||||
let locationIntervalId = null
|
||||
let device = null
|
||||
let sendTransport = null
|
||||
let producer = null
|
||||
const locationWatchId = ref(null)
|
||||
const locationIntervalId = ref(null)
|
||||
const device = ref(null)
|
||||
const sendTransport = ref(null)
|
||||
const producer = ref(null)
|
||||
|
||||
async function runFailureReasonCheck() {
|
||||
webrtcFailureReason.value = await getWebRTCFailureReason()
|
||||
@@ -194,8 +194,8 @@ async function startSharing() {
|
||||
const rtpCapabilities = await $fetch(`/api/live/webrtc/router-rtp-capabilities?sessionId=${sessionId.value}`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
device = await createMediasoupDevice(rtpCapabilities)
|
||||
sendTransport = await createSendTransport(device, sessionId.value, {
|
||||
device.value = await createMediasoupDevice(rtpCapabilities)
|
||||
sendTransport.value = await createSendTransport(device.value, sessionId.value, {
|
||||
onConnectSuccess: () => { webrtcState.value = 'connected' },
|
||||
onConnectFailure: () => {
|
||||
webrtcState.value = 'failed'
|
||||
@@ -208,31 +208,31 @@ async function startSharing() {
|
||||
if (!videoTrack) {
|
||||
throw new Error('No video track available')
|
||||
}
|
||||
producer = await sendTransport.produce({ track: videoTrack })
|
||||
producer.value = await sendTransport.value.produce({ track: videoTrack })
|
||||
// Monitor producer events
|
||||
producer.on('transportclose', () => {
|
||||
producer.value.on('transportclose', () => {
|
||||
logWarn('share-live: Producer transport closed', {
|
||||
producerId: producer.id,
|
||||
producerPaused: producer.paused,
|
||||
producerClosed: producer.closed,
|
||||
producerId: producer.value.id,
|
||||
producerPaused: producer.value.paused,
|
||||
producerClosed: producer.value.closed,
|
||||
})
|
||||
})
|
||||
producer.on('trackended', () => {
|
||||
producer.value.on('trackended', () => {
|
||||
logWarn('share-live: Producer track ended', {
|
||||
producerId: producer.id,
|
||||
producerPaused: producer.paused,
|
||||
producerClosed: producer.closed,
|
||||
producerId: producer.value.id,
|
||||
producerPaused: producer.value.paused,
|
||||
producerClosed: producer.value.closed,
|
||||
})
|
||||
})
|
||||
// Monitor transport state (mediasoup-client does not pass a parameter; read from transport.connectionState)
|
||||
sendTransport.on('connectionstatechange', () => {
|
||||
const state = sendTransport.connectionState
|
||||
sendTransport.value.on('connectionstatechange', () => {
|
||||
const state = sendTransport.value.connectionState
|
||||
if (state === 'connected') webrtcState.value = 'connected'
|
||||
else if (state === 'failed' || state === 'disconnected' || state === 'closed') {
|
||||
logWarn('share-live: Send transport connection state changed', {
|
||||
state,
|
||||
transportId: sendTransport.id,
|
||||
producerId: producer.id,
|
||||
transportId: sendTransport.value.id,
|
||||
producerId: producer.value.id,
|
||||
})
|
||||
if (state === 'failed') {
|
||||
webrtcState.value = 'failed'
|
||||
@@ -241,25 +241,25 @@ async function startSharing() {
|
||||
}
|
||||
})
|
||||
// Monitor track state
|
||||
if (producer.track) {
|
||||
producer.track.addEventListener('ended', () => {
|
||||
if (producer.value.track) {
|
||||
producer.value.track.addEventListener('ended', () => {
|
||||
logWarn('share-live: Producer track ended', {
|
||||
producerId: producer.id,
|
||||
trackId: producer.track.id,
|
||||
trackReadyState: producer.track.readyState,
|
||||
trackEnabled: producer.track.enabled,
|
||||
trackMuted: producer.track.muted,
|
||||
producerId: producer.value.id,
|
||||
trackId: producer.value.track.id,
|
||||
trackReadyState: producer.value.track.readyState,
|
||||
trackEnabled: producer.value.track.enabled,
|
||||
trackMuted: producer.value.track.muted,
|
||||
})
|
||||
})
|
||||
producer.track.addEventListener('mute', () => {
|
||||
producer.value.track.addEventListener('mute', () => {
|
||||
logWarn('share-live: Producer track muted', {
|
||||
producerId: producer.id,
|
||||
trackId: producer.track.id,
|
||||
trackEnabled: producer.track.enabled,
|
||||
trackMuted: producer.track.muted,
|
||||
producerId: producer.value.id,
|
||||
trackId: producer.value.track.id,
|
||||
trackEnabled: producer.value.track.enabled,
|
||||
trackMuted: producer.value.track.muted,
|
||||
})
|
||||
})
|
||||
producer.track.addEventListener('unmute', () => {})
|
||||
producer.value.track.addEventListener('unmute', () => {})
|
||||
}
|
||||
webrtcState.value = 'connected'
|
||||
setStatus('WebRTC connected. Requesting location…')
|
||||
@@ -273,7 +273,7 @@ async function startSharing() {
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Get location (continuous) — also requires HTTPS on mobile Safari
|
||||
// 5. Get location (continuous) - also requires HTTPS on mobile Safari
|
||||
if (!navigator.geolocation) {
|
||||
setError('Geolocation not supported in this browser.')
|
||||
cleanup()
|
||||
@@ -281,7 +281,7 @@ async function startSharing() {
|
||||
}
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
locationWatchId = navigator.geolocation.watchPosition(
|
||||
locationWatchId.value = navigator.geolocation.watchPosition(
|
||||
(pos) => {
|
||||
resolve(pos)
|
||||
},
|
||||
@@ -332,9 +332,9 @@ async function startSharing() {
|
||||
}
|
||||
catch (e) {
|
||||
if (e?.statusCode === 404) {
|
||||
if (locationIntervalId != null) {
|
||||
clearInterval(locationIntervalId)
|
||||
locationIntervalId = null
|
||||
if (locationIntervalId.value != null) {
|
||||
clearInterval(locationIntervalId.value)
|
||||
locationIntervalId.value = null
|
||||
}
|
||||
sharing.value = false
|
||||
if (!locationUpdate404Logged) {
|
||||
@@ -350,7 +350,7 @@ async function startSharing() {
|
||||
}
|
||||
|
||||
await sendLocationUpdate()
|
||||
locationIntervalId = setInterval(sendLocationUpdate, 2000)
|
||||
locationIntervalId.value = setInterval(sendLocationUpdate, 2000)
|
||||
}
|
||||
catch (e) {
|
||||
starting.value = false
|
||||
@@ -363,23 +363,23 @@ async function startSharing() {
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (locationWatchId != null && navigator.geolocation?.clearWatch) {
|
||||
navigator.geolocation.clearWatch(locationWatchId)
|
||||
if (locationWatchId.value != null && navigator.geolocation?.clearWatch) {
|
||||
navigator.geolocation.clearWatch(locationWatchId.value)
|
||||
}
|
||||
locationWatchId = null
|
||||
if (locationIntervalId != null) {
|
||||
clearInterval(locationIntervalId)
|
||||
locationWatchId.value = null
|
||||
if (locationIntervalId.value != null) {
|
||||
clearInterval(locationIntervalId.value)
|
||||
}
|
||||
locationIntervalId = null
|
||||
if (producer) {
|
||||
producer.close()
|
||||
producer = null
|
||||
locationIntervalId.value = null
|
||||
if (producer.value) {
|
||||
producer.value.close()
|
||||
producer.value = null
|
||||
}
|
||||
if (sendTransport) {
|
||||
sendTransport.close()
|
||||
sendTransport = null
|
||||
if (sendTransport.value) {
|
||||
sendTransport.value.close()
|
||||
sendTransport.value = null
|
||||
}
|
||||
device = null
|
||||
device.value = null
|
||||
if (stream.value) {
|
||||
stream.value.getTracks().forEach(t => t.stop())
|
||||
stream.value = null
|
||||
|
||||
Reference in New Issue
Block a user