diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..f579acb1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +**/node_modules/** +**/Dockerfile diff --git a/.env.template b/.env.template index 330f3865..a83bd171 100644 --- a/.env.template +++ b/.env.template @@ -6,3 +6,7 @@ JITSI_ISS= SECRET_JITSI_KEY= ADMIN_API_TOKEN=123 START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json +# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here. +# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. +# Keep empty if you are sharing hard coded / clear text credentials. +TURN_STATIC_AUTH_SECRET= diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index baa3a764..ce186cec 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -150,6 +150,7 @@ jobs: JITSI_ISS: ${{ secrets.JITSI_ISS }} JITSI_URL: ${{ secrets.JITSI_URL }} SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} + TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }} with: namespace: workadventure-${{ env.GITHUB_REF_SLUG }} @@ -159,5 +160,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com + msg: Environment deployed at https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com check_for_duplicate_msg: true diff --git a/README.md b/README.md index faafed98..a8c186b6 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/). # Work Adventure -## Work in progress - Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a 16-bit video game. @@ -15,7 +13,7 @@ In Work Adventure, you can move around your office and talk to your colleagues ( triggered when you move next to a colleague). -## Getting started +## Setting up a development environment Install Docker. @@ -101,5 +99,7 @@ Vagrant destroy * `Vagrant halt`: stop your VM Vagrant. * `Vagrant destroy`: delete your VM Vagrant. -## Features developed -You have more details of features developed in back [README.md](./back/README.md). +## Setting up a production environment + +The way you set up your production environment will highly depend on your servers. +We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory. diff --git a/back/Dockerfile b/back/Dockerfile index 5ec83a8f..e95145cd 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -1,16 +1,26 @@ -FROM thecodingmachine/workadventure-back-base:latest as builder -WORKDIR /var/www/messages -COPY --chown=docker:docker messages . +# protobuf build +FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder +WORKDIR /usr/src +COPY messages . RUN yarn install && yarn proto -FROM thecodingmachine/nodejs:12 - -COPY --chown=docker:docker back . -COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated +# typescript build +FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2 +WORKDIR /usr/src +COPY back/yarn.lock back/package.json ./ RUN yarn install - +COPY back . +COPY --from=builder /usr/src/generated src/Messages/generated ENV NODE_ENV=production RUN yarn run tsc -CMD ["yarn", "run", "runprod"] +# final production image +FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 +WORKDIR /usr/src +COPY back/yarn.lock back/package.json ./ +COPY --from=builder2 /usr/src/dist /usr/src/dist +ENV NODE_ENV=production +RUN yarn install --production +USER node +CMD ["yarn", "run", "runprod"] diff --git a/back/README.md b/back/README.md deleted file mode 100644 index 8a78f403..00000000 --- a/back/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Back Features - -## Login -To start your game, you must authenticate on the server back. -When you are authenticated, the back server return token and room starting. -``` -POST => /login -Params : - email: email of user. -``` - -## Join a room -When a user is connected, the user can join a room. -So you must send emit `join-room` with information user: -``` -Socket.io => 'join-room' - - userId: user id of gamer - roomId: room id when user enter in game - position: { - x: position x on map - y: position y on map - } -``` -All data users are stocked on socket client. - -## Send position user -When user move on the map, you can share new position on back with event `user-position`. -The information sent: -``` -Socket.io => 'user-position' - - userId: user id of gamer - roomId: room id when user enter in game - position: { - x: position x on map - y: position y on map - } -``` -All data users are updated on socket client. - -## Receive positions of all users -The application sends position of all users in each room in every few 10 milliseconds. -The data will pushed on event `user-position`: -``` -Socket.io => 'user-position' - - [ - { - userId: user id of gamer - roomId: room id when user enter in game - position: { - x: position x on map - y: position y on map - } - }, - ... - ] -``` - -[<<< back](../README.md) \ No newline at end of file diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 2cbfbf2e..b12f0542 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,4 +1,3 @@ -const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; @@ -12,9 +11,9 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed +export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; export { - SECRET_KEY, MINIMUM_DISTANCE, ADMIN_API_URL, ADMIN_API_TOKEN, diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index c90b51cf..3d6906ea 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {Group} from "../Model/Group"; import {cpuTracker} from "./CpuTracker"; -import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; +import { + GROUP_RADIUS, + JITSI_ISS, + MINIMUM_DISTANCE, + SECRET_JITSI_KEY, + TURN_STATIC_AUTH_SECRET +} from "../Enum/EnvironmentVariable"; import {Movable} from "../Model/Movable"; import {PositionInterface} from "../Model/PositionInterface"; import {adminApi, CharacterTexture} from "./AdminApi"; @@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager"; import {Zone} from "_Model/Zone"; import Debug from "debug"; import {Admin} from "_Model/Admin"; +import crypto from "crypto"; + const debug = Debug('sockermanager'); @@ -275,6 +283,12 @@ export class SocketManager { const webrtcSignalToClient = new WebRtcSignalToClientMessage(); webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); + // TODO: only compute credentials if data.signal.type === "offer" + if (TURN_STATIC_AUTH_SECRET !== '') { + const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + webrtcSignalToClient.setWebrtcusername(username); + webrtcSignalToClient.setWebrtcpassword(password); + } const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); @@ -295,6 +309,12 @@ export class SocketManager { const webrtcSignalToClient = new WebRtcSignalToClientMessage(); webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); + // TODO: only compute credentials if data.signal.type === "offer" + if (TURN_STATIC_AUTH_SECRET !== '') { + const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + webrtcSignalToClient.setWebrtcusername(username); + webrtcSignalToClient.setWebrtcpassword(password); + } const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); @@ -487,6 +507,11 @@ export class SocketManager { webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); + if (TURN_STATIC_AUTH_SECRET !== '') { + const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); + webrtcStartMessage1.setWebrtcusername(username); + webrtcStartMessage1.setWebrtcpassword(password); + } const serverToClientMessage1 = new ServerToClientMessage(); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); @@ -500,6 +525,11 @@ export class SocketManager { webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); + if (TURN_STATIC_AUTH_SECRET !== '') { + const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + webrtcStartMessage2.setWebrtcusername(username); + webrtcStartMessage2.setWebrtcpassword(password); + } const serverToClientMessage2 = new ServerToClientMessage(); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); @@ -512,6 +542,25 @@ export class SocketManager { } } + /** + * Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server + * and the Coturn server. + * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` + */ + private getTURNCredentials(name: string, secret: string): {username: string, password: string} { + const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours + const username = [unixTimeStamp, name].join(':'); + const hmac = crypto.createHmac('sha1', secret); + hmac.setEncoding('base64'); + hmac.write(username); + hmac.end(); + const password = hmac.read(); + return { + username: username, + password: password + }; + } + //disconnect user private disConnectedUser(user: User, group: Group) { // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection diff --git a/contrib/docker/.env.prod.template b/contrib/docker/.env.prod.template new file mode 100644 index 00000000..c0c10181 --- /dev/null +++ b/contrib/docker/.env.prod.template @@ -0,0 +1,20 @@ +# The base domain +DOMAIN=workadventure.localhost + +DEBUG_MODE=false +JITSI_URL=meet.jit.si +# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret +JITSI_PRIVATE_MODE=false +JITSI_ISS= +SECRET_JITSI_KEY= + +# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections) +TURN_SERVER= +TURN_USER= +TURN_PASSWORD= + +# The URL used by default, in the form: "/_/global/map/url.json" +START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json + +# The email address used by Let's encrypt to send renewal warnings (compulsory) +ACME_EMAIL= diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml new file mode 100644 index 00000000..22860748 --- /dev/null +++ b/contrib/docker/docker-compose.prod.yaml @@ -0,0 +1,100 @@ +version: "3.3" +services: + reverse-proxy: + image: traefik:v2.3 + command: + - --log.level=WARN + #- --api.insecure=true + - --providers.docker + - --entryPoints.web.address=:80 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entryPoints.websecure.address=:443 + - --certificatesresolvers.myresolver.acme.email=d.negrier@thecodingmachine.com + - --certificatesresolvers.myresolver.acme.storage=/acme.json + # used during the challenge + - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + ports: + - "80:80" + - "443:443" + # The Web UI (enabled by --api.insecure=true) + #- "8080:8080" + depends_on: + - pusher + - front + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./acme.json:/acme.json + restart: unless-stopped + + + front: + build: + context: ../.. + dockerfile: front/Dockerfile + #image: thecodingmachine/workadventure-front:master + environment: + DEBUG_MODE: "$DEBUG_MODE" + JITSI_URL: $JITSI_URL + JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" + API_URL: pusher.${DOMAIN} + TURN_SERVER: "${TURN_SERVER}" + TURN_USER: "${TURN_USER}" + TURN_PASSWORD: "${TURN_PASSWORD}" + START_ROOM_URL: "${START_ROOM_URL}" + labels: + - "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)" + - "traefik.http.routers.front.entryPoints=web,traefik" + - "traefik.http.services.front.loadbalancer.server.port=80" + - "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)" + - "traefik.http.routers.front-ssl.entryPoints=websecure" + - "traefik.http.routers.front-ssl.tls=true" + - "traefik.http.routers.front-ssl.service=front" + - "traefik.http.routers.front-ssl.tls.certresolver=myresolver" + restart: unless-stopped + + pusher: + build: + context: ../.. + dockerfile: pusher/Dockerfile + #image: thecodingmachine/workadventure-pusher:master + command: yarn run runprod + environment: + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + SECRET_KEY: yourSecretKey + API_URL: back:50051 + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + labels: + - "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)" + - "traefik.http.routers.pusher.entryPoints=web,traefik" + - "traefik.http.services.pusher.loadbalancer.server.port=8080" + - "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)" + - "traefik.http.routers.pusher-ssl.entryPoints=websecure" + - "traefik.http.routers.pusher-ssl.tls=true" + - "traefik.http.routers.pusher-ssl.service=pusher" + - "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver" + restart: unless-stopped + + back: + build: + context: ../.. + dockerfile: back/Dockerfile + #image: thecodingmachine/workadventure-back:master + command: yarn run runprod + environment: + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + ADMIN_API_URL: "$ADMIN_API_URL" + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + labels: + - "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)" + - "traefik.http.routers.back.entryPoints=web" + - "traefik.http.services.back.loadbalancer.server.port=8080" + - "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)" + - "traefik.http.routers.back-ssl.entryPoints=websecure" + - "traefik.http.routers.back-ssl.tls=true" + - "traefik.http.routers.back-ssl.service=back" + - "traefik.http.routers.back-ssl.tls.certresolver=myresolver" + restart: unless-stopped diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 9d201081..5093c86a 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -22,6 +22,7 @@ "JITSI_ISS": env.JITSI_ISS, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, + "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, } + if adminUrl != null then { "ADMIN_API_URL": adminUrl, } else {} @@ -40,6 +41,7 @@ "JITSI_ISS": env.JITSI_ISS, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, + "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, } + if adminUrl != null then { "ADMIN_API_URL": adminUrl, } else {} diff --git a/docker-compose.yaml b/docker-compose.yaml index 286c12ba..98071437 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -31,9 +31,12 @@ services: ADMIN_URL: workadventure.localhost STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_2: yarn install - TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443" - TURN_USER: workadventure - TURN_PASSWORD: WorkAdventure123 + STUN_SERVER: "stun:stun.l.google.com:19302" + TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349" + # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. + # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container + TURN_USER: "" + TURN_PASSWORD: "" START_ROOM_URL: "$START_ROOM_URL" command: yarn run start volumes: @@ -108,6 +111,7 @@ services: ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" JITSI_URL: $JITSI_URL JITSI_ISS: $JITSI_ISS + TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret volumes: - ./back:/usr/src/app labels: @@ -149,3 +153,28 @@ services: - ./back:/usr/src/back - ./front:/usr/src/front - ./pusher:/usr/src/pusher + +# coturn: +# image: coturn/coturn:4.5.2 +# command: +# - turnserver +# #- -c=/etc/coturn/turnserver.conf +# - --log-file=stdout +# - --external-ip=$$(detect-external-ip) +# - --listening-port=3478 +# - --min-port=10000 +# - --max-port=10010 +# - --tls-listening-port=5349 +# - --listening-ip=0.0.0.0 +# - --realm=coturn.workadventure.localhost +# - --server-name=coturn.workadventure.localhost +# - --lt-cred-mech +# # Enable Coturn "REST API" to validate temporary passwords. +# #- --use-auth-secret +# #- --static-auth-secret=SomeStaticAuthSecret +# #- --userdb=/var/lib/turn/turndb +# - --user=workadventure:WorkAdventure123 +# # use real-valid certificate/privatekey files +# #- --cert=/root/letsencrypt/fullchain.pem +# #- --pkey=/root/letsencrypt/privkey.pem +# network_mode: host diff --git a/front/dist/index.html.tmpl b/front/dist/index.tmpl.html similarity index 100% rename from front/dist/index.html.tmpl rename to front/dist/index.tmpl.html diff --git a/front/dist/resources/html/gameReport.html b/front/dist/resources/html/gameReport.html index 9a761c32..59ca3592 100644 --- a/front/dist/resources/html/gameReport.html +++ b/front/dist/resources/html/gameReport.html @@ -12,10 +12,6 @@ border-radius: 6px; margin: 2px auto 0; width: 298px; - height: 220px; - } - #gameReport .cautiousText { - font-size: 50%; } #gameReport h1 { background-image: linear-gradient(top, #f1f3f3, #d4dae0); @@ -30,6 +26,9 @@ text-align: center; text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff; } + #gameReport h3 { + margin: 0; + } #gameReport textarea { font-size: 70%; background: linear-gradient(top, #d6d7d7, #dee0e0); @@ -51,15 +50,17 @@ } #gameReport button { margin-top: 10px; - background-color: black; + font-size: 60%; + background-color: #dc3545; color: white; border-radius: 7px; - padding-bottom: 4px; - width: 60px; + padding: 3px 10px 3px 10px; } #gameReport button#gameReportFormCancel { background-color: #c7c7c700; color: #292929; + display: block; + float: right; } #gameReport section a{ text-align: center; @@ -74,8 +75,11 @@ #gameReport section.text-center{ text-align: center; } - #gameReport section p{ + #gameReport p{ font-size: 8px; + margin: 3px 0 0 0; + } + #gameReport form p{ margin: 0px 70px; } #gameReport section p.err{ @@ -87,18 +91,32 @@ } -