server.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. const http = require('http')
  2. const fs = require('fs')
  3. const path = require('path')
  4. const { WebSocketServer } = require('ws')
  5. const HOST = '0.0.0.0'
  6. const PORT = 17865
  7. const WS_PATH = '/mock-gps'
  8. const PROXY_PATH = '/proxy'
  9. const PUBLIC_DIR = path.join(__dirname, 'public')
  10. function getContentType(filePath) {
  11. const ext = path.extname(filePath).toLowerCase()
  12. if (ext === '.html') {
  13. return 'text/html; charset=utf-8'
  14. }
  15. if (ext === '.css') {
  16. return 'text/css; charset=utf-8'
  17. }
  18. if (ext === '.js') {
  19. return 'application/javascript; charset=utf-8'
  20. }
  21. if (ext === '.json') {
  22. return 'application/json; charset=utf-8'
  23. }
  24. if (ext === '.svg') {
  25. return 'image/svg+xml'
  26. }
  27. return 'text/plain; charset=utf-8'
  28. }
  29. function serveStatic(requestPath, response) {
  30. const safePath = requestPath === '/' ? '/index.html' : requestPath
  31. const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
  32. if (!resolvedPath.startsWith(PUBLIC_DIR)) {
  33. response.writeHead(403)
  34. response.end('Forbidden')
  35. return
  36. }
  37. fs.readFile(resolvedPath, (error, content) => {
  38. if (error) {
  39. response.writeHead(404)
  40. response.end('Not Found')
  41. return
  42. }
  43. response.writeHead(200, {
  44. 'Content-Type': getContentType(resolvedPath),
  45. 'Cache-Control': 'no-store',
  46. })
  47. response.end(content)
  48. })
  49. }
  50. function isMockGpsPayload(payload) {
  51. return payload
  52. && payload.type === 'mock_gps'
  53. && Number.isFinite(payload.lat)
  54. && Number.isFinite(payload.lon)
  55. }
  56. function isMockHeartRatePayload(payload) {
  57. return payload
  58. && payload.type === 'mock_heart_rate'
  59. && Number.isFinite(payload.bpm)
  60. }
  61. async function handleProxyRequest(request, response) {
  62. const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${PORT}`)
  63. const targetUrl = requestUrl.searchParams.get('url')
  64. if (!targetUrl) {
  65. response.writeHead(400, {
  66. 'Content-Type': 'text/plain; charset=utf-8',
  67. 'Access-Control-Allow-Origin': '*',
  68. })
  69. response.end('Missing url')
  70. return
  71. }
  72. try {
  73. const upstream = await fetch(targetUrl)
  74. const body = Buffer.from(await upstream.arrayBuffer())
  75. response.writeHead(upstream.status, {
  76. 'Content-Type': upstream.headers.get('content-type') || 'application/octet-stream',
  77. 'Cache-Control': 'no-store',
  78. 'Access-Control-Allow-Origin': '*',
  79. })
  80. response.end(body)
  81. } catch (error) {
  82. response.writeHead(502, {
  83. 'Content-Type': 'text/plain; charset=utf-8',
  84. 'Access-Control-Allow-Origin': '*',
  85. })
  86. response.end(error && error.message ? error.message : 'Proxy request failed')
  87. }
  88. }
  89. const server = http.createServer((request, response) => {
  90. if ((request.url || '').startsWith(PROXY_PATH)) {
  91. handleProxyRequest(request, response)
  92. return
  93. }
  94. serveStatic(request.url || '/', response)
  95. })
  96. const wss = new WebSocketServer({ noServer: true })
  97. wss.on('connection', (socket) => {
  98. socket.on('message', (rawMessage) => {
  99. const text = String(rawMessage)
  100. let parsed
  101. try {
  102. parsed = JSON.parse(text)
  103. } catch (_error) {
  104. return
  105. }
  106. if (!isMockGpsPayload(parsed) && !isMockHeartRatePayload(parsed)) {
  107. return
  108. }
  109. const serialized = isMockGpsPayload(parsed)
  110. ? JSON.stringify({
  111. type: 'mock_gps',
  112. timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
  113. lat: Number(parsed.lat),
  114. lon: Number(parsed.lon),
  115. accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
  116. speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
  117. headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
  118. })
  119. : JSON.stringify({
  120. type: 'mock_heart_rate',
  121. timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
  122. bpm: Math.max(1, Math.round(Number(parsed.bpm))),
  123. })
  124. wss.clients.forEach((client) => {
  125. if (client.readyState === client.OPEN) {
  126. client.send(serialized)
  127. }
  128. })
  129. })
  130. })
  131. server.on('upgrade', (request, socket, head) => {
  132. if (!request.url || !request.url.startsWith(WS_PATH)) {
  133. socket.destroy()
  134. return
  135. }
  136. wss.handleUpgrade(request, socket, head, (ws) => {
  137. wss.emit('connection', ws, request)
  138. })
  139. })
  140. server.listen(PORT, HOST, () => {
  141. console.log(`Mock GPS simulator running:`)
  142. console.log(` UI: http://127.0.0.1:${PORT}/`)
  143. console.log(` WS: ws://127.0.0.1:${PORT}${WS_PATH}`)
  144. console.log(` Proxy: http://127.0.0.1:${PORT}${PROXY_PATH}?url=<remote-url>`)
  145. })