server.cjs (26472B)
1 const ws = require('ws'); 2 const http = require('node:http'); 3 const url = require('url'); 4 const jwt = require('jsonwebtoken'); 5 const nodemailer = require('nodemailer'); 6 const db = require('./db.cjs'); 7 8 require('dotenv').config({ 9 // Configure common config files and modes 10 // - https://www.npmjs.com/package/dotenv 11 // - https://vitejs.dev/guide/env-and-mode 12 path: [ 13 '.env.local', 14 '.env.development', '.env.development.local', 15 '.env.test', '.env.test.local', 16 '.env.production', '.env.production.local', 17 '.env' 18 ] 19 }); 20 21 // Read environment and set defaults 22 api_uri = process.env.VITE_API_URI; 23 api_port = process.env.VITE_API_PORT; 24 redirect_uri = process.env.API_REDIRECT_URI; 25 cors_allow_origin = process.env.API_CORS_ALLOW_ORIGIN; 26 jwt_secret = process.env.API_JWT_SECRET; 27 jwt_ttl = process.env.API_JWT_TTL; 28 29 if (jwt_secret === undefined) { 30 console.log('Balls, API_JWT_SECRET undefined π³'); 31 process.exit() 32 } else if (process.env.SMTP_HOST === undefined || 33 process.env.SMTP_PORT === undefined || 34 process.env.SMTP_STARTTLS === undefined || 35 process.env.SMTP_USER === undefined || 36 process.env.SMTP_FROM === undefined || 37 process.env.SMTP_PASSWORD === undefined) { 38 console.log('Balls, Not all SMTP settings defined π³'); 39 process.exit() 40 } 41 42 // Configure mail transport 43 // https://nodemailer.com/smtp 44 const transport = nodemailer.createTransport({ 45 host: process.env.SMTP_HOST, 46 port: process.env.SMTP_PORT, 47 secure: process.env.SMTP_STARTTLS, 48 auth: { 49 user: process.env.SMTP_USER, 50 pass: process.env.SMTP_PASSWORD, 51 }, 52 }); 53 54 // Define API paths and allowed request methods 55 const paths = [ 56 '/v1/healthz', 57 '/v1/auth/verify', 58 '/v1/echo', 59 '/v1/auth/requestMagicLink', 60 '/v1/auth/invalidateToken', // not implemented 61 '/v1/leaderboard/allHeats', 62 '/v1/leaderboard/allAthletes', // π authenticated 63 '/v1/leaderboard/newHeat', // π authenticated 64 '/v1/leaderboard/getHeat', // π authenticated 65 '/v1/leaderboard/removeHeat', // π authenticated 66 '/v1/leaderboard/distinctStartlist', 67 '/v1/leaderboard/startlistWithAthletes', // π authenticated 68 '/v1/leaderboard/scoresForHeatAndAthlete', 69 '/v1/leaderboard/scoreSummaryForHeatAndAthlete', 70 '/v1/leaderboard/getScore', // π authenticated 71 '/v1/leaderboard/setScore', // π authenticated 72 '/v1/leaderboard/addAthleteToHeat', // π authenticated 73 '/v1/leaderboard/removeAthleteFromHeat', // π authenticated 74 '/v1/leaderboard/addAthlete', // π authenticated 75 '/v1/leaderboard/removeAthlete', // π authenticated 76 '/v1/leaderboard/allSettings', 77 '/v1/leaderboard/getSetting', // π authenticated 78 '/v1/leaderboard/updateSetting', // π authenticated 79 '/v1/leaderboard/removeSetting', // π authenticated 80 ] 81 82 console.log("Backend API:", api_uri); 83 console.log("Backend API port:", api_port); 84 console.log("Redirect URI:", redirect_uri); 85 console.log("CORS Allow Origin:", cors_allow_origin); 86 console.log("SMTP server:", process.env.SMTP_HOST); 87 console.log("SMTP port:", process.env.SMTP_PORT); 88 console.log("SMTP starttls:", process.env.SMTP_STARTTLS); 89 console.log("* * * * * * *", "Uncle roger ready π", "* * * * * * *"); 90 91 // Create a simple http API server 92 // - https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction 93 // - https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener 94 const server = http.createServer(); 95 96 // Listen on request events 97 server.on('request', async (req, res) => { 98 const url = new URL(`${api_uri}:${api_port}${req.url}`); 99 const search_params = url.searchParams; 100 101 console.log('> Path', url.pathname); 102 console.log(' Method', req.method); 103 104 // set headers 105 res.setHeader('Content-Type', 'application/json'); 106 res.setHeader('Access-Control-Allow-Origin', cors_allow_origin); 107 108 // cors pre-flight request uses options method 109 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 110 res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization'); 111 112 // extact authorization token 113 const authHeader = req.headers.authorization 114 let token = undefined 115 try { 116 if (authHeader.startsWith("Bearer ")) { 117 token = authHeader.substring(7, authHeader.length) 118 console.log(' Bearer token', token); 119 } 120 } catch(error) { 121 console.warn("x Warning: No authorization bearer token in request header") 122 //serverError(res, new Error("No authorization bearer token in request header")); 123 //return 124 } 125 126 // cors pre-flight request uses options method 127 if (req.method === 'OPTIONS') { 128 res.end() 129 } else if (req.method === 'GET') { 130 switch(url.pathname) { 131 case '/v1/healthz': 132 res.end(JSON.stringify({ 133 message: 'egg fried rice π fuiyooh!', 134 })); 135 break 136 case '/v1/auth/verify': 137 try { 138 token = search_params.get('token'); 139 let user = await verifyToken(req, token) 140 res.end(JSON.stringify({ 141 message: 'Uncle roger verified π€ fuuiiyooh!', 142 data: user, 143 })); 144 } catch (error) { 145 serverError(res, error); 146 } 147 break 148 case '/v1/leaderboard/allHeats': 149 try { 150 const heats = await db.allHeats() 151 152 res.end(JSON.stringify({ 153 message: 'All heats', 154 data: heats, 155 })); 156 } catch(error) { 157 serverError(res, error); 158 } 159 break 160 case '/v1/leaderboard/allAthletes': 161 try { 162 await verifyToken(req, token) 163 const athletes = await db.allAthletes() 164 165 res.end(JSON.stringify({ 166 message: 'All athletes', 167 data: athletes, 168 })); 169 } catch(error) { 170 serverError(res, error); 171 } 172 break 173 case '/v1/leaderboard/getSetting': 174 try { 175 await verifyToken(req, token) 176 177 let name = search_params.get('name'); 178 const setting = await db.getSetting(name) 179 180 if (setting.length < 1) { 181 noContent(res); 182 return 183 } 184 res.end(JSON.stringify({ 185 message: `Setting with name ${name}`, 186 data: setting[0].value, 187 })); 188 } catch(error) { 189 serverError(res, error); 190 } 191 break 192 case '/v1/leaderboard/allSettings': 193 try { 194 const settings = await db.allSettings() 195 196 if (settings.length < 1) { 197 noContent(res); 198 return 199 } 200 res.end(JSON.stringify({ 201 message: 'All settings', 202 data: settings, 203 })); 204 } catch(error) { 205 serverError(res, error); 206 } 207 break 208 default: 209 const pathExists = paths.find((i) => i === url.pathname); 210 if (pathExists) { 211 // wrong method for this path 212 notAllowed(res, req.method); 213 } else { 214 notFound(res, url.pathname); 215 } 216 } // end switch 217 } else if (req.method === 'POST') { 218 let body = []; 219 220 switch(url.pathname) { 221 case '/v1/echo': 222 req.on('data', chunk => { 223 body.push(chunk); 224 }).on('end', () => { 225 body = JSON.stringify({ 226 message: 'Uncle roger knows do egg fried rice since 3 years old, lah', 227 data: Buffer.concat(body).toString(), 228 }); 229 res.end(body); 230 }) 231 break 232 case '/v1/auth/requestMagicLink': 233 req.on('data', chunk => { 234 body.push(chunk); 235 }).on('end', async () => { 236 const b = Buffer.concat(body); 237 try { 238 if (b.length < 1) { 239 throw new Error("Empty request body") 240 } 241 input = JSON.parse(b); 242 console.log(' Link request for:', input.email); 243 244 const users = await db.getUser(input.email); 245 if (users.length < 1) { 246 throw new Error("User not found") 247 } 248 249 const user = users[0] 250 251 // create magic link 252 const exp = new Date(Date.now() + 1000 * jwt_ttl) 253 const token = jwt.sign({ email: user.email }, jwt_secret, { 254 expiresIn: 1000 * jwt_ttl, 255 }) 256 const magic_link = `${redirect_uri}?token=${token}` 257 const magic_href = `${api_uri}:${api_port}/v1/auth/verify?token=${token}` 258 console.log("πMagic href:", magic_href) 259 260 // save token with expiration time 261 await db.saveToken(user.id, token, exp) 262 263 // send magic link to user 264 await transport.sendMail({ 265 from: process.env.SMTP_FROM, 266 to: user.email, 267 subject: 'MyHeats Magic Link', 268 //text: `Click here β¨ to log in: ${magic_link}`, 269 html: `Click <a href="${magic_link}">here</a> β¨ to log in.`, 270 }) 271 272 res.end(JSON.stringify({ 273 message: 'Magic link requested', 274 })); 275 } catch (error) { 276 serverError(res, error); 277 } 278 }) 279 break 280 case '/v1/leaderboard/distinctStartlist': 281 req.on('data', chunk => { 282 body.push(chunk); 283 }).on('end', async () => { 284 const b = Buffer.concat(body); 285 try { 286 if (b.length < 1) { 287 throw new Error("Empty request body") 288 } 289 input = JSON.parse(b); 290 console.log(' distinctStartlist request with headIds:', input.heat_ids); 291 292 const startlist = await db.distinctStartlist(input.heat_ids); 293 294 if (startlist.length < 1) { 295 noContent(res); 296 return 297 } 298 res.end(JSON.stringify({ 299 message: 'Distinct athletes for multiple heats', 300 data: startlist, 301 })); 302 } catch(error) { 303 serverError(res, error); 304 } 305 }) 306 break 307 case '/v1/leaderboard/startlistWithAthletes': 308 req.on('data', chunk => { 309 body.push(chunk); 310 }).on('end', async () => { 311 const b = Buffer.concat(body); 312 try { 313 await verifyToken(req, token) 314 if (b.length < 1) { 315 throw new Error("Empty request body") 316 } 317 input = JSON.parse(b); 318 console.log(' startlistWithAthletes request with headId:', input.heat_id); 319 320 const startlist = await db.startlistWithAthletes(input.heat_id); 321 322 if (startlist.length < 1) { 323 throw new Error("No athletes for this startlist") 324 } 325 res.end(JSON.stringify({ 326 message: 'Startlist with athletes for heat', 327 data: startlist, 328 })); 329 } catch(error) { 330 serverError(res, error); 331 } 332 }) 333 break 334 case '/v1/leaderboard/scoresForHeatAndAthlete': 335 req.on('data', chunk => { 336 body.push(chunk); 337 }).on('end', async () => { 338 const b = Buffer.concat(body); 339 try { 340 if (b.length < 1) { 341 throw new Error("Empty request body") 342 } 343 input = JSON.parse(b); 344 console.log(' scoresForHeatAndAthlete request with heat and athlete:', 345 input.heat, input.athlete); 346 347 const scores = await db.scoresForHeatAndAthlete( 348 input.heat, 349 input.athlete 350 ) 351 352 if (scores.length < 1) { 353 noContent(res); 354 return 355 } 356 res.end(JSON.stringify({ 357 message: 'Scores for heat and athlete', 358 data: scores, 359 })); 360 } catch(error) { 361 serverError(res, error); 362 } 363 }) 364 break 365 case '/v1/leaderboard/scoreSummaryForHeatAndAthlete': 366 req.on('data', chunk => { 367 body.push(chunk); 368 }).on('end', async () => { 369 const b = Buffer.concat(body); 370 try { 371 if (b.length < 1) { 372 throw new Error("Empty request body") 373 } 374 input = JSON.parse(b); 375 console.log(' scoreSummaryForHeatAndAthlete request with heat and athlete:', 376 input.heat, input.athlete); 377 378 const summary = await db.scoreSummaryForHeatAndAthlete( 379 input.heat, 380 input.athlete 381 ) 382 383 if (summary.length < 1) { 384 noContent(res); 385 return 386 } 387 res.end(JSON.stringify({ 388 message: 'Score summary for heat and athlete', 389 data: summary[0], 390 })); 391 } catch(error) { 392 serverError(res, error); 393 } 394 }) 395 break 396 case '/v1/leaderboard/getScore': 397 req.on('data', chunk => { 398 body.push(chunk); 399 }).on('end', async () => { 400 const b = Buffer.concat(body); 401 try { 402 await verifyToken(req, token) 403 if (b.length < 1) { 404 throw new Error("Empty request body") 405 } 406 input = JSON.parse(b); 407 console.log(' GetScore request for:', input); 408 409 const scores = await db.getScore( 410 input.heat, 411 input.athlete, 412 input.judge 413 ); 414 415 if (scores.length < 1) { 416 noContent(res); 417 return 418 } 419 420 res.end(JSON.stringify({ 421 message: 'Requested score for heat, user and judge', 422 data: scores[0], 423 })); 424 } catch (error) { 425 serverError(res, error); 426 } 427 }) 428 break 429 case '/v1/leaderboard/setScore': 430 req.on('data', chunk => { 431 body.push(chunk); 432 }).on('end', async () => { 433 const b = Buffer.concat(body); 434 try { 435 await verifyToken(req, token) 436 if (b.length < 1) { 437 throw new Error("Empty request body") 438 } 439 input = JSON.parse(b); 440 console.log(' SetScore request for:', input); 441 442 const scores = await db.setScore( 443 input.heat, 444 input.athlete, 445 input.judge, 446 input.score, 447 ); 448 449 if (scores.length < 1) { 450 throw new Error("Score not updated") 451 } 452 453 res.end(JSON.stringify({ 454 message: 'Score update for heat, user and judge', 455 data: scores[0], 456 })); 457 } catch (error) { 458 serverError(res, error); 459 } 460 }) 461 break 462 case '/v1/leaderboard/newHeat': 463 req.on('data', chunk => { 464 body.push(chunk); 465 }).on('end', async () => { 466 const b = Buffer.concat(body); 467 try { 468 await verifyToken(req, token) 469 if (b.length < 1) { 470 throw new Error("Empty request body") 471 } 472 input = JSON.parse(b); 473 console.log(' newHeat request for:', input); 474 475 const heats = await db.newHeat( 476 input.name, 477 input.location, 478 input.planned_start, 479 ); 480 if (heats.length < 1) { 481 throw new Error("Heat not created") 482 } 483 484 res.end(JSON.stringify({ 485 message: 'New heat', 486 data: heats[0], 487 })); 488 } catch (error) { 489 serverError(res, error); 490 } 491 }) 492 break 493 case '/v1/leaderboard/getHeat': 494 req.on('data', chunk => { 495 body.push(chunk); 496 }).on('end', async () => { 497 const b = Buffer.concat(body); 498 try { 499 await verifyToken(req, token) 500 if (b.length < 1) { 501 throw new Error("Empty request body") 502 } 503 input = JSON.parse(b); 504 console.log(' getHeat request for:', input); 505 506 const heats = await db.getHeat( 507 input.heat_id, 508 ); 509 if (heats.length < 1) { 510 throw new Error("Heat not found") 511 } 512 513 res.end(JSON.stringify({ 514 message: 'New heat', 515 data: heats[0], 516 })); 517 } catch (error) { 518 serverError(res, error); 519 } 520 }) 521 break 522 case '/v1/leaderboard/removeHeat': 523 req.on('data', chunk => { 524 body.push(chunk); 525 }).on('end', async () => { 526 const b = Buffer.concat(body); 527 try { 528 await verifyToken(req, token) 529 if (b.length < 1) { 530 throw new Error("Empty request body") 531 } 532 input = JSON.parse(b); 533 console.log(' removeHeat request for:', input); 534 535 const heats = await db.removeHeat(input.heat_id) 536 if (heats.length < 1) { 537 throw new Error("Heat not removed") 538 } 539 res.end(JSON.stringify({ 540 message: 'Heat removed', 541 data: heats[0], 542 })); 543 } catch (error) { 544 serverError(res, error); 545 } 546 }) 547 break 548 case '/v1/leaderboard/addAthleteToHeat': 549 req.on('data', chunk => { 550 body.push(chunk); 551 }).on('end', async () => { 552 const b = Buffer.concat(body); 553 try { 554 await verifyToken(req, token) 555 if (b.length < 1) { 556 throw new Error("Empty request body") 557 } 558 input = JSON.parse(b); 559 console.log(' addAthleteToHeat request for:', input); 560 561 const startlist = await db.addAthleteToHeat( 562 input.athlete, 563 input.heat, 564 ); 565 if (startlist.length < 1) { 566 throw new Error("Startlist not updated") 567 } 568 569 res.end(JSON.stringify({ 570 message: 'Athlete added to startlist', 571 data: startlist[0], 572 })); 573 } catch (error) { 574 serverError(res, error); 575 } 576 }) 577 break 578 case '/v1/leaderboard/removeAthleteFromHeat': 579 req.on('data', chunk => { 580 body.push(chunk); 581 }).on('end', async () => { 582 const b = Buffer.concat(body); 583 try { 584 await verifyToken(req, token) 585 if (b.length < 1) { 586 throw new Error("Empty request body") 587 } 588 input = JSON.parse(b); 589 console.log(' removeAthleteFromHeat request for:', input); 590 591 const startlist = await db.removeAthleteFromHeat(input.startlist_id) 592 if (startlist.length < 1) { 593 throw new Error("Startlist not updated") 594 } 595 596 res.end(JSON.stringify({ 597 message: 'Athlete removed from startlist', 598 data: startlist[0], 599 })); 600 } catch (error) { 601 serverError(res, error); 602 } 603 }) 604 break 605 case '/v1/leaderboard/addAthlete': 606 req.on('data', chunk => { 607 body.push(chunk); 608 }).on('end', async () => { 609 const b = Buffer.concat(body); 610 try { 611 await verifyToken(req, token) 612 if (b.length < 1) { 613 throw new Error("Empty request body") 614 } 615 input = JSON.parse(b); 616 console.log(' addAthlete request for:', input); 617 618 const athlete = await db.addAthlete( 619 input.nr, 620 input.firstname, 621 input.lastname, 622 input.birthday, 623 input.school, 624 ); 625 if (athlete.length < 1) { 626 throw new Error("Startlist not updated") 627 } 628 629 res.end(JSON.stringify({ 630 message: 'Athlete created', 631 data: athlete[0] 632 })); 633 } catch (error) { 634 serverError(res, error); 635 } 636 }) 637 break 638 case '/v1/leaderboard/removeAthlete': 639 req.on('data', chunk => { 640 body.push(chunk); 641 }).on('end', async () => { 642 const b = Buffer.concat(body); 643 try { 644 await verifyToken(req, token) 645 if (b.length < 1) { 646 throw new Error("Empty request body") 647 } 648 input = JSON.parse(b); 649 console.log(' removeAthlete request for:', input); 650 651 const athlete = await db.removeAthlete(input.athlete_id) 652 if (athlete.length < 1) { 653 throw new Error("Athlete not removed") 654 } 655 res.end(JSON.stringify({ 656 message: 'Athlete removed', 657 data: athlete[0], 658 })); 659 } catch (error) { 660 serverError(res, error); 661 } 662 }) 663 break 664 case '/v1/leaderboard/updateSetting': 665 req.on('data', chunk => { 666 body.push(chunk); 667 }).on('end', async () => { 668 const b = Buffer.concat(body); 669 try { 670 await verifyToken(req, token) 671 if (b.length < 1) { 672 throw new Error("Empty request body") 673 } 674 input = JSON.parse(b); 675 console.log(' updateSetting request for:', input); 676 677 const setting = await db.updateSetting(input.name, input.value) 678 if (setting.length < 1) { 679 throw new Error("Setting not updated") 680 } 681 res.end(JSON.stringify({ 682 message: 'Setting updated', 683 data: setting[0], 684 })); 685 } catch (error) { 686 serverError(res, error); 687 } 688 }) 689 break 690 case '/v1/leaderboard/removeSetting': 691 req.on('data', chunk => { 692 body.push(chunk); 693 }).on('end', async () => { 694 const b = Buffer.concat(body); 695 try { 696 await verifyToken(req, token) 697 if (b.length < 1) { 698 throw new Error("Empty request body") 699 } 700 input = JSON.parse(b); 701 console.log(' removeSetting request for:', input); 702 703 const settings = await db.removeSetting(input.name) 704 if (settings.length < 1) { 705 throw new Error("Setting not removed") 706 } 707 res.end(JSON.stringify({ 708 message: 'Setting removed', 709 data: settings[0], 710 })); 711 } catch (error) { 712 serverError(res, error); 713 } 714 }) 715 break 716 default: 717 const pathExists = paths.find((i) => i === url.pathname); 718 if (pathExists) { 719 // wrong method for this path 720 notAllowed(res, req.method); 721 } else { 722 notFound(res, url.pathname); 723 } 724 } // end switch 725 } else { 726 notAllowed(res, req.method); 727 } 728 }); 729 730 // returns user on success, throws error otherwise 731 async function verifyToken(req, token) { 732 if (!token){ 733 throw {message: "No token found in verifyToken"} 734 } else if (token.length === 0) { 735 throw {message: "Token empty in verifyToken"} 736 } 737 738 // lookup token in the database 739 const users = await db.lookupToken(token); 740 if (users.length < 1) { 741 throw {message: "User not found in verifyToken"} 742 } 743 744 const user = users[0] 745 746 console.warn(" Token expires_at:", users[0].expires_at); 747 console.log(" Time now, lah:", new Date()); 748 749 // check expiration date of token in the database 750 if (new Date(users[0].expires_at) < new Date()) { 751 throw new Error('Token expired.. tthink about yu saad live πΏ') 752 } 753 754 // verify token signature 755 const v = jwt.verify(token, jwt_secret); 756 //await db.invalidateToken(token); 757 console.log(" Verified token", v); 758 759 if (v.email !== user.email) { 760 throw new Error("Token signature invalid") 761 } 762 return user 763 } 764 765 function noContent(res) { 766 console.warn('x Warning: 204 no rice π±'); 767 res.statusCode = 204; 768 res.end(); 769 } 770 function notFound(res, path) { 771 console.error('x Error: 404 not found'); 772 res.statusCode = 404; 773 res.end(JSON.stringify({ 774 message: 'no tea cup π΅ use fingaa π haaiiyaa!', 775 error: `404 path ${path} not found`, 776 })); 777 } 778 function notAllowed(res, method) { 779 console.error('x Error: 403 method', method, 'not allowed'); 780 res.statusCode = 403; 781 res.end(JSON.stringify({ 782 message: 'why english tea π« cup to measure rice? Use fingaa π haaiiyaa!', 783 error: `403 method ${method} not allowed`, 784 })); 785 } 786 function serverError(res, err) { 787 console.error('x Error:', err); 788 res.statusCode = 500; 789 res.end(JSON.stringify({ 790 message: 'no colander, haaiiyaa, rice π wet you fucked up!', 791 error: err.message, 792 })); 793 } 794 795 // Create websocket listeners 796 // https://github.com/websockets/ws?tab=readme-ov-file#multiple-servers-sharing-a-single-https-server 797 const wss1 = new ws.WebSocketServer({ noServer: true}); 798 799 // Listen for websocket connections 800 wss1.on('connection', function connection(sock) { 801 sock.on('message', async function message(m) { 802 try { 803 msg = JSON.parse(m) 804 console.log(' Uncle roger hears:', msg); 805 806 if (msg.method === 'watchScores') { 807 console.log(`~ Client requesting new scores`); 808 db.watchScores(sock) 809 } 810 } catch (error) { 811 console.error('x Error:', error.message); 812 } 813 }); 814 815 sock.on('error', console.error); 816 817 // Client closes the connection 818 sock.on('close', function(event) { 819 console.log(" Close event:", event.code); 820 console.log(" Close reason:", event.reason); 821 db.removeClient(sock); 822 }); 823 824 console.log(`~ Received a new websocket connection`); 825 db.addClient(sock) 826 }); 827 828 // Listen to upgrade event 829 server.on('upgrade', function upgrade(req, sock, head) { 830 const url = new URL(`${api_uri}:${api_port}${req.url}`); 831 832 console.log('> WS path', url.pathname); 833 834 if (url.pathname === '/v1/leaderboard') { 835 wss1.handleUpgrade(req, sock, head, function done(sock) { 836 wss1.emit('connection', sock, req); 837 }); 838 } else { 839 sock.destroy(); 840 } 841 }); 842 843 // Listen and serve 844 server.listen(api_port);