diff --git a/.gitignore b/.gitignore index 3c3629e..57d8ced 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +*.env \ No newline at end of file diff --git a/api-server/Dockerfile.prod b/api-server/Dockerfile.prod new file mode 100644 index 0000000..e751529 --- /dev/null +++ b/api-server/Dockerfile.prod @@ -0,0 +1,12 @@ +FROM python:3.10.10 + +WORKDIR /app/server + +# install packages +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install gunicorn==23.0.0 gevent==24.2.1 gevent-websocket==0.10.1 + +COPY . . + +CMD ["gunicorn", "-k", "geventwebsocket.gunicorn.workers.GeventWebSocketWorker", "-w", "1", "--threads", "100", "-b", "0.0.0.0:8080", "app:app"] \ No newline at end of file diff --git a/api-server/app.py b/api-server/app.py index 12c3b08..8a392c7 100644 --- a/api-server/app.py +++ b/api-server/app.py @@ -4,11 +4,12 @@ from chat import setup_chat from webrtc import setup_webrtc import uuid +import os from utils.Debugger import Debugger app = Flask(__name__) -CORS(app) -socketio = SocketIO(app, cors_allowed_origins="*") +CORS(app, supports_credentials=True) +socketio = SocketIO(app, cors_allowed_origins="*", manage_session=False) # In-memory storage for each session session_storage = {} @@ -35,18 +36,21 @@ def create_meeting(): @app.route('/api/session/', methods=['GET']) def get_session(meeting_id): + Debugger.log_message('DEBUG', f'{session_storage}') if meeting_id not in session_storage: return jsonify({'error': 'Meeting ID not found'}), 404 return jsonify(session_storage[meeting_id]) @app.route('/api/users/', methods=['GET']) def get_users(meeting_id): + Debugger.log_message('DEBUG', f'{session_storage}') if meeting_id not in session_storage: return jsonify({'error': 'Meeting ID not found'}), 404 return jsonify(list(session_storage[meeting_id]['users'].keys())) @socketio.on('join') def handle_join(data): + Debugger.log_message('DEBUG', f'{session_storage}') if 'username' not in data or 'meeting_id' not in data: Debugger.log_message('ERROR', f'Join request missing username or meeting ID: {data}') emit('error', {'message': 'Missing username or meeting ID'}, to=request.sid) @@ -59,6 +63,7 @@ def handle_join(data): session['users'][username] = request.sid Debugger.log_message('INFO', f'User {username} joined the meeting', meeting_id) + Debugger.log_message('DEBUG', f'{session_storage}') emit('user_joined', {'username': username, 'meeting_id': meeting_id}, room=meeting_id) @socketio.on('disconnect') @@ -80,8 +85,9 @@ def handle_disconnect(): setup_webrtc(app, socketio, session_storage, Debugger.log_message) if __name__ == '__main__': - import os if os.getenv('TESTING', True): socketio.run(app, debug=True, allow_unsafe_werkzeug=True) + elif os.getenv('PRODUCTION', True): + socketio.run(app, async_mode='gevent') else: socketio.run(app, debug=True) \ No newline at end of file diff --git a/api-server/chat.py b/api-server/chat.py index 65ec838..62e8e88 100644 --- a/api-server/chat.py +++ b/api-server/chat.py @@ -1,15 +1,18 @@ from flask import jsonify, request from flask_socketio import emit +from utils.Debugger import Debugger def setup_chat(app, socketio, session_storage, log_message): @app.route('/api/chat_history/', methods=['GET']) def get_chat_history(meeting_id): + Debugger.log_message('DEBUG', f'{session_storage}') if meeting_id not in session_storage: return jsonify({'error': 'Meeting ID not found'}), 404 return jsonify(session_storage[meeting_id]['chat_history']) @socketio.on('chat_message') def handle_chat_message(data): + Debugger.log_message('DEBUG', f'{session_storage}') meeting_id = data['meeting_id'] sender = data['sender'] message = data['text'] diff --git a/client/package-lock.json b/client/package-lock.json index a4a1f2b..7a50f1b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7956,15 +7956,6 @@ "tslib": "^2.0.3" } }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", @@ -16648,6 +16639,14 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index d1de62f..88c7659 100644 --- a/client/package.json +++ b/client/package.json @@ -2,6 +2,7 @@ "name": "client", "version": "0.1.0", "private": true, + "proxy": "http://localhost:5000", "dependencies": { "@headlessui/react": "^2.1.3", "@heroicons/react": "^2.1.5", diff --git a/client/src/services/rtcHandler.js b/client/src/services/rtcHandler.js index cd8979c..ccb87be 100644 --- a/client/src/services/rtcHandler.js +++ b/client/src/services/rtcHandler.js @@ -62,7 +62,32 @@ class RTCHandler { } const pc = new RTCPeerConnection({ - iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + iceServers: [ + { + urls: "stun:stun.relay.metered.ca:80", + }, + { + urls: "turn:global.relay.metered.ca:80", + username: process.env.REACT_APP_TURN_SERVER_USERNAME, + credential: process.env.REACT_APP_TURN_SERVER_CREDENTIALS, + }, + { + urls: "turn:global.relay.metered.ca:80?transport=tcp", + username: process.env.REACT_APP_TURN_SERVER_USERNAME, + credential: process.env.REACT_APP_TURN_SERVER_CREDENTIALS, + }, + { + urls: "turn:global.relay.metered.ca:443", + username: process.env.REACT_APP_TURN_SERVER_USERNAME, + credential: process.env.REACT_APP_TURN_SERVER_CREDENTIALS, + }, + { + urls: "turns:global.relay.metered.ca:443?transport=tcp", + username: process.env.REACT_APP_TURN_SERVER_USERNAME, + credential: process.env.REACT_APP_TURN_SERVER_CREDENTIALS, + }, + { urls: 'stun:stun.l.google.com:19302' }, + ] }); pc.onicecandidate = (event) => this.handleICECandidateEvent(event, peerUsername); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..123b984 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,36 @@ +services: + api_server: + build: + context: ./api-server + dockerfile: Dockerfile.prod + container_name: api_server + environment: + - PRODUCTION=true + networks: + - app_network + + nginx: + container_name: nginx + image: jonasal/nginx-certbot + restart: always + environment: + - CERTBOT_EMAIL=e256zhan@uwaterloo.ca + - CERTBOT_DOMAINS=vmeet.duckdns.org + ports: + - 80:80 + - 443:443 + volumes: + - nginx_secrets:/etc/letsencrypt + - ./user_conf.d:/etc/nginx/user_conf.d + - ./client/build:/usr/share/nginx/html + depends_on: + - api_server + networks: + - app_network + +volumes: + nginx_secrets: + +networks: + app_network: + driver: bridge \ No newline at end of file diff --git a/prod.sh b/prod.sh new file mode 100755 index 0000000..734fa05 --- /dev/null +++ b/prod.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +export REACT_APP_API_URL="https://vmeet.duckdns.org" +export PRODUCTION="true" + +(cd client && rm -rf build && npm run build) + +DOWN_COMMAND="down --remove-orphans" +if command -v docker-compose &> /dev/null; then + docker-compose $DOWN_COMMAND +elif command -v docker compose &> /dev/null; then + docker compose $DOWN_COMMAND +else + echo "Error: Neither 'docker-compose' nor 'docker compose' is installed." + exit 1 +fi + +COMPOSE_COMMAND="-f docker-compose.prod.yml up --build -d" + +if command -v docker-compose &> /dev/null; then + docker-compose $COMPOSE_COMMAND "$@" +elif command -v docker compose &> /dev/null; then + docker compose $COMPOSE_COMMAND "$@" +else + echo "Error: Neither 'docker-compose' nor 'docker compose' is installed." + exit 1 +fi diff --git a/user_conf.d/vmeet.conf b/user_conf.d/vmeet.conf new file mode 100644 index 0000000..0779579 --- /dev/null +++ b/user_conf.d/vmeet.conf @@ -0,0 +1,63 @@ +server { + listen 80; + server_name vmeet.duckdns.org; + + # Redirect all HTTP traffic to HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name vmeet.duckdns.org; + + # SSL certificates (use Let's Encrypt certificates or any other trusted provider) + ssl_certificate /etc/letsencrypt/live/vmeet.duckdns.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/vmeet.duckdns.org/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # Serve React app (single-page application) + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; + } + + # Proxy settings for API requests + location /api/ { + proxy_pass http://api_server:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy settings for WebSocket connections + location /socket.io/ { + proxy_pass http://api_server:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_buffering off; + } + + # Additional settings for better security (optional) + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Increase buffer and timeout settings if needed + client_max_body_size 100M; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +}