diff --git a/.gitignore b/.gitignore index 763301f..dc27c73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dist/ -node_modules/ \ No newline at end of file +node_modules/ +config/* +!config/*.example.* \ No newline at end of file diff --git a/README.md b/README.md index 1927748..e5fb14b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ * [Getting Started](#getting-started) * [Prerequisites](#prerequisites) * [Installation](#installation) + * [Customizing Peer to Peer Behavior](#customizing-peer-to-peer-behavior) * [Contributing](#contributing) * [License](#license) @@ -54,7 +55,7 @@ To get up and running quickly, you can deploy to Heroku using the button below [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) -This will deploy an instance of the crewlink-server. You can get the URL of your server by using the app name that you gave when you launched the app on heroku and appending `.herokuapp.com`. You can also find the URL of your server by going to "Settings", scrolling down to "Domains", and removing the `https://` and trailing slash from the url. Using this URL, follow step 4 of the [installation instructions](https://github.com/ottomated/CrewLink-server#manual-installation) to connect your client to your server instance. +This will deploy an instance of the crewlink-server. You can get the URL of your server by using the app name that you gave when you launched the app on heroku and appending `.herokuapp.com`. You can also find the URL of your server by going to "Settings", scrolling down to "Domains". Using this URL, follow step 4 of the [installation instructions](https://github.com/ottomated/CrewLink-server#manual-installation) to connect your client to your server instance. ## Docker Quickstart @@ -111,8 +112,30 @@ yarn install ```JS yarn start ``` -4. Copy your server's IP and port into CrewLink settings. Make sure everyone in your lobby is using the same server. - +4. Copy your server URL into CrewLink settings. Make sure everyone in your lobby is using the same server. +### Customizing Peer to Peer Behavior +CrewLink-server should work out of the box for most people. You may, however, come across people who are unable to hear +other people, no matter what they try. This could be because of a NAT or firewall that is preventing peer to peer +connections. In this case you may want to set up your own relay server to act as a middleman for your voice traffic. +CrewLink needs a specific type of server for this called a TURN server. + +You can configure a TURN server by creating a file called ``peerConfig.yml`` in the config folder. Use the +provided example as a template. A good, open source TURN server implementation is +[Coturn](https://github.com/coturn/coturn). A relay server may also be desirable in case you want to prevent CrewLink +players from detecting the IP addresses of everyone in the same Among Us room using your voice server. + +CAUTION: Hosting your own TURN server is a great way to solve most connection issues but they're not without problems. +These servers, by design, let anyone with the credentials relay packets to anywhere. Because you are sending the TURN +credentials to everyone connecting to your CrewLink server, obtaining them becomes very easy. If you choose to set one +up, make sure you: + - Know what you're doing + - Configure the server in a way that disallows relaying packets to places that make no sense (localhost, private networks, etc) + - Consider limiting the bandwidth per user and across the whole TURN server + - Keep an eye on TURN server activity while you're running one + - Change TURN credentials regularly + - This list is not exhaustive, see point 1 + +It is because of this that you should only deploy a TURN server for small, private CrewLink servers. ## Contributing diff --git a/config/peerConfig.example.yml b/config/peerConfig.example.yml new file mode 100644 index 0000000..0af014a --- /dev/null +++ b/config/peerConfig.example.yml @@ -0,0 +1,35 @@ +# IN ORDER TO USE THIS CONFIG FILE, RENAME IT AND REMOVE THE ".example" PART OF THE FILENAME! +# +# CrewLink peer to peer connection configuration. +# +# This file lets you modify how CrewLink connects players to each other. +# +# CrewLink uses peer to peer connections to send voice and game state data between players. This means that every player +# has to be able to connect to every other player some way or another in order for CrewLink to work. Below you can tweak +# the mechanisms CrewLink uses to establish peer to peer connections. + +# Force CrewLink to only work through TURN servers. This is great if you want to protect the IP addresses of players +# as no direct connection will be made. At least one TURN server is required if set to true. +forceRelayOnly: false + +# If you run into connection issues, you may want to add your own TURN server here. +# +# CAUTION: Hosting your own TURN server is a great way to solve most connection issues but they're not without problems. +# These servers, by design, let anyone with the credentials relay packets to anywhere. Because you are sending the TURN +# credentials to everyone connecting to your CrewLink server, obtaining them becomes very easy. If you choose to set one +# up, make sure you: +# - Know what you're doing +# - Configure the server in a way that disallows relaying packets to places that make no sense (localhost, private networks, etc) +# - Consider limiting the bandwidth per user and across the whole TURN server +# - Keep an eye on TURN server activity while you're running one +# - Change TURN credentials regularly +# - This list is not exhaustive, see point 1 +iceServers: + # Google's STUN server is used by default + - urls: 'stun:stun.l.google.com:19302' +# - urls: 'turn:example.com' +# username: 'TurnUsername' +# credential: 'TurnPassword' +# - urls: 'stun:example.com' +# username: 'StunUsername' +# credential: 'StunPassword' \ No newline at end of file diff --git a/package.json b/package.json index 6f7cb1b..9ed8c09 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "morgan": "^1.10.0", "pug": "^3.0.0", "socket.io": "^2.3.0", - "tracer": "^1.1.4" + "tracer": "^1.1.4", + "yaml": "^1.10.0" }, "scripts": { "start": "yarn compile && node dist/index.js", diff --git a/src/ICEServer.ts b/src/ICEServer.ts new file mode 100644 index 0000000..9bacc96 --- /dev/null +++ b/src/ICEServer.ts @@ -0,0 +1,5 @@ +export interface ICEServer { + urls: string|string[]; + username?: string; + credential?: string; +} diff --git a/src/index.ts b/src/index.ts index 39f0d0e..6785489 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,13 @@ import express from 'express'; import { Server } from 'http'; import { Server as HttpsServer } from 'https'; -import { readFileSync, readdirSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'path'; import socketIO from 'socket.io'; import Tracer from 'tracer'; import morgan from 'morgan'; +import peerConfig from './peerConfig'; +import { ICEServer } from './ICEServer'; const supportedCrewLinkVersions = new Set(['1.2.0', '1.2.1']); const httpsEnabled = !!process.env.HTTPS; @@ -28,6 +30,13 @@ if (httpsEnabled) { } else { server = new Server(app); } + +let address = process.env.ADDRESS; +if (!address) { + logger.error('You must set the ADDRESS environment variable.'); + process.exit(1); +} + const io = socketIO(server); const clients = new Map(); @@ -42,15 +51,15 @@ interface Signal { to: string; } -app.set('view engine', 'pug') -app.use(morgan('combined')) +interface ClientPeerConfig { + forceRelayOnly: boolean; + iceServers: ICEServer[] +} + +app.set('view engine', 'pug'); +app.use(morgan('combined')); let connectionCount = 0; -let address = process.env.ADDRESS; -if (!address) { - logger.error('You must set the ADDRESS environment variable.'); - process.exit(1); -} app.get('/', (_, res) => { res.render('index', { connectionCount, address }); @@ -88,6 +97,13 @@ io.on('connection', (socket: socketIO.Socket) => { logger.info("Total connected: %d", connectionCount); let code: string | null = null; + const clientPeerConfig: ClientPeerConfig = { + forceRelayOnly: peerConfig.forceRelayOnly, + iceServers: peerConfig.iceServers? [...peerConfig.iceServers] : [] + } + + socket.emit('clientPeerConfig', clientPeerConfig); + socket.on('join', (c: string, id: number, clientId: number) => { if (typeof c !== 'string' || typeof id !== 'number' || typeof clientId !== 'number') { socket.disconnect(); @@ -163,10 +179,7 @@ io.on('connection', (socket: socketIO.Socket) => { connectionCount--; logger.info("Total connected: %d", connectionCount); }) - -}) +}); server.listen(port); -(async () => { - logger.info('CrewLink Server started: %s', address); -})(); +logger.info('CrewLink Server started: %s', address); diff --git a/src/peerConfig.ts b/src/peerConfig.ts new file mode 100644 index 0000000..5e33ac5 --- /dev/null +++ b/src/peerConfig.ts @@ -0,0 +1,31 @@ +import YAML from 'yaml'; +import path from 'path'; +import fs from 'fs'; +import { ICEServer } from './ICEServer'; + +const PEER_CONFIG_PATH = path.join(__dirname, '..', 'config', 'peerConfig.yml'); + +interface PeerConfig { + forceRelayOnly: boolean; + iceServers?: ICEServer[] +} + +const DEFAULT_PEER_CONFIG: PeerConfig = { + forceRelayOnly: false, + iceServers: [ + { + urls: 'stun:stun.l.google.com:19302' + } + ] +}; + +let peerConfig = DEFAULT_PEER_CONFIG; +if (fs.existsSync(PEER_CONFIG_PATH)) { + try { + peerConfig = YAML.parse(fs.readFileSync(PEER_CONFIG_PATH).toString('utf8')); + } catch (err) { + console.error(`Unable to load peer config file. Make sure it is valid YAML.\n${err}`); + } +} + +export default peerConfig diff --git a/yarn.lock b/yarn.lock index 49fbdf8..89c79a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,22 +2,22 @@ # yarn lockfile v1 -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" - integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" + integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== "@babel/types@^7.6.1", "@babel/types@^7.9.6": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" - integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" + integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-validator-identifier" "^7.12.11" lodash "^4.17.19" to-fast-properties "^2.0.0" @@ -30,9 +30,9 @@ "@types/node" "*" "@types/connect@*": - version "3.4.33" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" - integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + version "3.4.34" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" + integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== dependencies: "@types/node" "*" @@ -44,18 +44,18 @@ "@types/node" "*" "@types/express-serve-static-core@*": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" - integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz#6ba02465165b6c9c3d8db3a28def6b16fc9b70f5" + integrity sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/express@^4.17.8": - version "4.17.8" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" - integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.9.tgz#f5f2df6add703ff28428add52bdec8a1091b0a78" + integrity sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "*" @@ -75,9 +75,9 @@ "@types/node" "*" "@types/node@*": - version "14.14.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.3.tgz#e1c09064121f894baaad2bd9f12ce4a41bffb274" - integrity sha512-33/L34xS7HVUx23e0wOT2V1qPF1IrHgQccdJVm9uXGTB9vFBrrzBtkQymT8VskeKOxjz55MSqMv0xuLq+u98WQ== + version "14.14.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.16.tgz#3cc351f8d48101deadfed4c9e4f116048d437b4b" + integrity sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw== "@types/qs@*": version "6.9.5" @@ -90,20 +90,28 @@ integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== "@types/serve-static@*": - version "1.13.6" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" - integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA== + version "1.13.8" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46" + integrity sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA== dependencies: "@types/mime" "*" "@types/node" "*" +"@types/socket.io-parser@*": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/socket.io-parser/-/socket.io-parser-2.2.1.tgz#dc94aed303839487f4975249a32a548109ea3647" + integrity sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ== + dependencies: + "@types/node" "*" + "@types/socket.io@^2.1.11": - version "2.1.11" - resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.11.tgz#e0d6759880e5f9818d5297a3328b36641bae996b" - integrity sha512-bVprmqPhJMLb9ZCm8g0Xy8kwBFRbnanOWSxzWkDkkIwxTvud5tKMfAJymXX6LQbizUKCS1yima7JM4BeLqjNqA== + version "2.1.12" + resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.12.tgz#91187f826a5dd2ed1113e935815bbaf9627e5cb0" + integrity sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA== dependencies: "@types/engine.io" "*" "@types/node" "*" + "@types/socket.io-parser" "*" accepts@~1.3.4, accepts@~1.3.7: version "1.3.7" @@ -639,9 +647,9 @@ ms@2.1.1: integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== negotiator@0.6.2: version "0.6.2" @@ -1003,9 +1011,9 @@ type-is@~1.6.17, type-is@~1.6.18: mime-types "~2.1.24" typescript@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" - integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" @@ -1038,9 +1046,9 @@ with@^7.0.0: babel-walk "3.0.0-canary-5" ws@^7.1.2: - version "7.3.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" - integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + version "7.4.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.1.tgz#a333be02696bd0e54cea0434e21dcc8a9ac294bb" + integrity sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ== ws@~6.1.0: version "6.1.4" @@ -1054,6 +1062,11 @@ xmlhttprequest-ssl@~1.5.4: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= +yaml@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"