Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Productionize #26

Merged
merged 7 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
*.env
12 changes: 12 additions & 0 deletions api-server/Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -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"]
12 changes: 9 additions & 3 deletions api-server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand All @@ -35,18 +36,21 @@ def create_meeting():

@app.route('/api/session/<meeting_id>', 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/<meeting_id>', 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)
Expand All @@ -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')
Expand All @@ -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)
3 changes: 3 additions & 0 deletions api-server/chat.py
Original file line number Diff line number Diff line change
@@ -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/<meeting_id>', 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']
Expand Down
17 changes: 8 additions & 9 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 26 additions & 1 deletion client/src/services/rtcHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -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:
- [email protected]
- 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
27 changes: 27 additions & 0 deletions prod.sh
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions user_conf.d/vmeet.conf
Original file line number Diff line number Diff line change
@@ -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;
}
Loading