[call-me] - init commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
swagger: '2.0'
|
||||
|
||||
info:
|
||||
title: Call-me API
|
||||
description: API description for external applications that integrates with Call-me.
|
||||
version: 1.0.0
|
||||
|
||||
basePath: /api/v1
|
||||
|
||||
schemes:
|
||||
- https
|
||||
- http
|
||||
|
||||
paths:
|
||||
/users:
|
||||
get:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Connected users'
|
||||
description: 'Get all connected users'
|
||||
consumes:
|
||||
- 'application/json'
|
||||
produces:
|
||||
- 'application/json'
|
||||
security:
|
||||
- secretApiKey: []
|
||||
responses:
|
||||
'200':
|
||||
description: 'Connected users'
|
||||
schema:
|
||||
$ref: '#/definitions/UsersResponse'
|
||||
'403':
|
||||
description: 'Unauthorized!'
|
||||
|
||||
securityDefinitions:
|
||||
secretApiKey:
|
||||
type: 'apiKey'
|
||||
name: 'authorization'
|
||||
in: 'header'
|
||||
description: 'Format like this: authorization: {API_KEY_SECRET}'
|
||||
|
||||
definitions:
|
||||
UsersResponse:
|
||||
type: 'object'
|
||||
properties:
|
||||
users:
|
||||
type: 'array'
|
||||
Executable
+272
@@ -0,0 +1,272 @@
|
||||
'use strict';
|
||||
|
||||
// Import necessary modules
|
||||
const dotenv = require('dotenv').config();
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const socketIO = require('socket.io');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
|
||||
// Public directory location
|
||||
const PUBLIC_DIR = path.join(__dirname, '../', 'public');
|
||||
|
||||
// Map to store connected users
|
||||
const users = new Map();
|
||||
|
||||
// Configuration settings
|
||||
const config = {
|
||||
iceServers: [],
|
||||
stunServerEnabled: process.env.STUN_SERVER_ENABLED === 'true',
|
||||
stunServerUrl: process.env.STUN_SERVER_URL,
|
||||
turnServerEnabled: process.env.TURN_SERVER_ENABLED === 'true',
|
||||
turnServerUrl: process.env.TURN_SERVER_URL,
|
||||
turnServerUsername: process.env.TURN_SERVER_USERNAME,
|
||||
turnServerCredential: process.env.TURN_SERVER_CREDENTIAL,
|
||||
apiKeySecret: process.env.API_KEY_SECRET,
|
||||
apiBasePath: '/api/v1',
|
||||
swaggerDocument: yaml.load(fs.readFileSync(path.join(__dirname, '/api/swagger.yaml'), 'utf8')),
|
||||
};
|
||||
|
||||
// Add STUN server if enabled and URL is provided
|
||||
if (config.stunServerEnabled && config.stunServerUrl) {
|
||||
config.iceServers.push({ urls: config.stunServerUrl });
|
||||
}
|
||||
|
||||
// Add TURN server if enabled and all required information is provided
|
||||
if (config.turnServerEnabled && config.turnServerUrl && config.turnServerUsername && config.turnServerCredential) {
|
||||
config.iceServers.push({
|
||||
urls: config.turnServerUrl,
|
||||
username: config.turnServerUsername,
|
||||
credential: config.turnServerCredential,
|
||||
});
|
||||
}
|
||||
|
||||
// Create Express application
|
||||
const app = express();
|
||||
|
||||
// Server configurations
|
||||
const domain = process.env.DOMAIN || 'localhost';
|
||||
const isHttps = process.env.SSL === 'true';
|
||||
const port = process.env.PORT || 4000;
|
||||
const host = `http${isHttps ? 's' : ''}://${domain}:${port}`;
|
||||
const apiDocs = host + config.apiBasePath + '/docs';
|
||||
|
||||
// This server
|
||||
let server;
|
||||
|
||||
// Load self-signed certificates if HTTPS is enabled
|
||||
if (isHttps) {
|
||||
try {
|
||||
const options = {
|
||||
key: fs.readFileSync(path.join(__dirname, '/ssl/key.pem'), 'utf-8'),
|
||||
cert: fs.readFileSync(path.join(__dirname, '/ssl/cert.pem'), 'utf-8'),
|
||||
};
|
||||
// Create HTTPS server using Express
|
||||
server = https.createServer(options, app);
|
||||
} catch (err) {
|
||||
console.error('Error loading certificates:', err);
|
||||
process.exit(1); // Exit the process if certificates cannot be loaded
|
||||
}
|
||||
} else {
|
||||
// Create HTTP server using Express
|
||||
server = http.createServer(app);
|
||||
}
|
||||
|
||||
// Create WebSocket server using Socket.io on top of HTTP server
|
||||
const io = socketIO(server);
|
||||
|
||||
// Start the server and listen on the specified port
|
||||
server.listen(port, () => {
|
||||
console.log('Server', {
|
||||
running_at: host,
|
||||
ice: config.iceServers,
|
||||
api_key_secret: config.apiKeySecret,
|
||||
api_docs: apiDocs,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle WebSocket connections
|
||||
io.on('connection', handleConnection);
|
||||
|
||||
app.use(express.static(PUBLIC_DIR)); // Serve static files from the 'public' directory
|
||||
app.use(express.json()); // Api parse body data as json
|
||||
app.use(config.apiBasePath + '/docs', swaggerUi.serve, swaggerUi.setup(config.swaggerDocument)); // api docs
|
||||
|
||||
// Logs requests
|
||||
app.use((req, res, next) => {
|
||||
console.log('New request', {
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Set up route to serve the main HTML file
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(PUBLIC_DIR, '/index.html'));
|
||||
});
|
||||
|
||||
// Axios API requests
|
||||
app.get(`${config.apiBasePath}/users`, (req, res) => {
|
||||
// check if user is authorized for the API call
|
||||
const { authorization } = req.headers;
|
||||
console.log(authorization);
|
||||
if (authorization != config.apiKeySecret) {
|
||||
console.log('Unauthorized API call: Get Users', {
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
});
|
||||
return res.status(403).json({ error: 'Unauthorized!' });
|
||||
}
|
||||
// Get connected users
|
||||
const users = getConnectedUsers();
|
||||
return res.json({ users });
|
||||
});
|
||||
|
||||
// Function to handle individual WebSocket connections
|
||||
function handleConnection(socket) {
|
||||
console.log('User connected:', socket.id);
|
||||
|
||||
// Send a ping message to the newly connected client
|
||||
sendPing(socket);
|
||||
|
||||
// Set up event listeners for incoming messages and disconnect
|
||||
socket.on('message', handleMessage);
|
||||
socket.on('disconnect', handleClose);
|
||||
|
||||
// Function to handle incoming messages
|
||||
function handleMessage(data) {
|
||||
const { type } = data;
|
||||
|
||||
console.log('Received message:', type);
|
||||
|
||||
switch (type) {
|
||||
case 'signIn':
|
||||
handleSignIn(data);
|
||||
break;
|
||||
case 'offerAccept':
|
||||
case 'offerDecline':
|
||||
case 'offerCreate':
|
||||
handleOffer(data);
|
||||
break;
|
||||
case 'offer':
|
||||
case 'answer':
|
||||
case 'candidate':
|
||||
case 'leave':
|
||||
handleSignalingMessage(data);
|
||||
break;
|
||||
case 'pong':
|
||||
console.log('Client response:', data.message);
|
||||
break;
|
||||
default:
|
||||
sendError(socket, `Unknown command: ${type}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Send a ping message to the newly connected client and iceServers for peer connection
|
||||
function sendPing(socket) {
|
||||
sendMsgTo(socket, {
|
||||
type: 'ping',
|
||||
message: 'Hello Client!',
|
||||
iceServers: config.iceServers,
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle user sign-in request
|
||||
function handleSignIn(data) {
|
||||
const { name } = data;
|
||||
if (!users.has(name)) {
|
||||
users.set(name, socket);
|
||||
socket.username = name;
|
||||
console.log('User signed in:', name);
|
||||
sendMsgTo(socket, { type: 'signIn', success: true });
|
||||
console.log('Users', getConnectedUsers());
|
||||
} else {
|
||||
sendMsgTo(socket, { type: 'signIn', success: false, message: 'Username already in use' });
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle offer request
|
||||
function handleOffer(data) {
|
||||
console.log('handleOffer', data);
|
||||
|
||||
const { from, to, name, type } = data;
|
||||
const toName = type === 'offerAccept' ? to : from;
|
||||
const recipientSocket = users.get(toName);
|
||||
|
||||
console.log(`Handling offer for ${toName}`);
|
||||
|
||||
switch (type) {
|
||||
case 'offerAccept':
|
||||
case 'offerCreate':
|
||||
if (recipientSocket) {
|
||||
sendMsgTo(recipientSocket, data);
|
||||
} else {
|
||||
console.warn(`Recipient (${toName}) not found`);
|
||||
sendError(socket, `User ${toName} not found`);
|
||||
}
|
||||
break;
|
||||
case 'offerDecline':
|
||||
console.warn(`User ${name} declined your call`);
|
||||
sendError(recipientSocket || socket, `User ${name} declined your call`);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown offer type: ${type}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle signaling messages (offer, answer, candidate, leave)
|
||||
function handleSignalingMessage(data) {
|
||||
const { name } = data;
|
||||
const recipientSocket = users.get(name);
|
||||
if (recipientSocket) {
|
||||
sendMsgTo(recipientSocket, { ...data, name: socket.username });
|
||||
} else {
|
||||
sendError(socket, `User ${name} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle the closing of a connection
|
||||
function handleClose() {
|
||||
if (socket.username) {
|
||||
console.log('User disconnected:', socket.username);
|
||||
users.delete(socket.username);
|
||||
const recipientSocket = users.get(socket.recipient);
|
||||
if (recipientSocket) {
|
||||
sendMsgTo(recipientSocket, { type: 'leave' });
|
||||
}
|
||||
console.log('Users', getConnectedUsers());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get all connected users
|
||||
function getConnectedUsers() {
|
||||
return Array.from(users.keys());
|
||||
}
|
||||
|
||||
// Function to broadcast a message to all connection
|
||||
function broadcastMsg(socket, message) {
|
||||
console.log('Broadcast message:', message.type);
|
||||
socket.broadcast.emit('message', message);
|
||||
}
|
||||
|
||||
// Function to send a message to a specific connection
|
||||
function sendMsgTo(socket, message) {
|
||||
console.log('Sending message:', message.type);
|
||||
socket.emit('message', message);
|
||||
}
|
||||
|
||||
// Function to send an error message to a specific connection
|
||||
function sendError(socket, message) {
|
||||
console.error('Error:', message);
|
||||
sendMsgTo(socket, { type: 'error', message: message });
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
## Call-me SSL
|
||||
|
||||

|
||||
|
||||
1. Generate a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate)
|
||||
|
||||
```bash
|
||||
# install openssl 4 ubuntu
|
||||
apt install openssl
|
||||
|
||||
# install openssl 4 mac
|
||||
brew install openssl
|
||||
|
||||
# self-signed certificate
|
||||
openssl genrsa -out key.pem
|
||||
openssl req -new -key key.pem -out csr.pem
|
||||
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
|
||||
rm csr.pem
|
||||
|
||||
# https://www.sslchecker.com/certdecoder
|
||||
```
|
||||
|
||||
2. Expose `server.js` on `https` using the `self-signed certificate`, edit the `.env` file
|
||||
|
||||
```bash
|
||||
SSL=true
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDcTCCAlkCFBqR3KLE2n3uKEy+2GmqGQPmrWDmMA0GCSqGSIb3DQEBCwUAMHQx
|
||||
CzAJBgNVBAYTAklUMQ4wDAYDVQQIDAVJdGFseTEQMA4GA1UECgwHQ2FsbC1tZTEX
|
||||
MBUGA1UEAwwOTWlyb3NsYXYgUGVqaWMxKjAoBgkqhkiG9w0BCQEWG21pcm9zbGF2
|
||||
LnBlamljLjg1QGdtYWlsLmNvbTAgFw0yNDAzMDgwODM0MzNaGA8yMDUxMDcyNDA4
|
||||
MzQzM1owdDELMAkGA1UEBhMCSVQxDjAMBgNVBAgMBUl0YWx5MRAwDgYDVQQKDAdD
|
||||
YWxsLW1lMRcwFQYDVQQDDA5NaXJvc2xhdiBQZWppYzEqMCgGCSqGSIb3DQEJARYb
|
||||
bWlyb3NsYXYucGVqaWMuODVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAwVWgx7jwFD/XmvIDOgkl7thhxC97fuDOEs6CDGB2KBtykfM6
|
||||
xtrXDYryS7ZQgzba27JTABd5CIv/9tBAt6orYVtQmj5ABBTJjJ+zVCYpyoW9NRzs
|
||||
0vejatoKksbovd1mr8QHrZJUvR6e5lXvg+rK311fAHo5WWBPJwwbB6z9mxtHQWuU
|
||||
OOfjGaYxOyuNXbVDKZYC8c8LxxowwZ7VXt3gt+mIYLh99iUXOaD7jusljz87ZlGx
|
||||
JKpbL5C4qXAx0nB4UH0XeZTrPz6NHXHn0RpAn33IztbSPcf8iOJO7JkaZwNBGrDW
|
||||
RzlpKWas57Mb1I0u7A38oFMdS/BTaWkq5E8diwIDAQABMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQAtUmzF798rFWXc5c8K/QtHaB0oqnadC3FvvQDVDI8OtYzE3Cnst32CKFTn
|
||||
ueFmCShWryKVnKgszTjrQfuKFwj5U5EKnlwMwmjKzB1bHLj0hn2Olzq4f5IJw3pp
|
||||
pAKPhvgZxI4PPv+Q6xaGYvmn3jeNx5UlL5Xb6JkC1pUIJayj8HBSyuT47poXny1J
|
||||
Cyi2nK8kuG2hXUyHK02DVr39wx+ir84gpkafAHQYm9dlCyQ+YXXxq28egxSrenCH
|
||||
wwNSXWLN9LO4T4OGJNa+inyya+y7QvhNYDUkWhsXwZCuP0YLPiSwor7vi/hSw2NL
|
||||
E29H8F6yW3sOND7e9wlpEWkN4ZGy
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAwVWgx7jwFD/XmvIDOgkl7thhxC97fuDOEs6CDGB2KBtykfM6
|
||||
xtrXDYryS7ZQgzba27JTABd5CIv/9tBAt6orYVtQmj5ABBTJjJ+zVCYpyoW9NRzs
|
||||
0vejatoKksbovd1mr8QHrZJUvR6e5lXvg+rK311fAHo5WWBPJwwbB6z9mxtHQWuU
|
||||
OOfjGaYxOyuNXbVDKZYC8c8LxxowwZ7VXt3gt+mIYLh99iUXOaD7jusljz87ZlGx
|
||||
JKpbL5C4qXAx0nB4UH0XeZTrPz6NHXHn0RpAn33IztbSPcf8iOJO7JkaZwNBGrDW
|
||||
RzlpKWas57Mb1I0u7A38oFMdS/BTaWkq5E8diwIDAQABAoIBAGVx3M9vhunZJfSz
|
||||
kGXfbvwIpiQfFhnZM8hCe5Kq+tTIsjFLA0hchAMjKUf4/f4bxnMI+uxNZMDLVR/Y
|
||||
3lyA+go3s+97QnPhxG/5TEuXSs5tpn48EzgPoLvsdSfXH0Cg78TCg5Tb94LUkmQi
|
||||
K294v8K8Z6EEgRsYoe/HNlesudWsa/MRGuslkxSsi8MpBK0PnH3sT7X4lIGHWW2V
|
||||
EL+tZ8FXvzFdeIddwn8f4S2OOzYD3ZPq1/afoH1tlB++IInc60a+9m8tTdnbfAYU
|
||||
JDGolz02IVzuxts6ybeVilNnqX4cl/awZdZ3EySfE9XUb9OP+HCuO3u3yzhOoGWr
|
||||
JhMqKuECgYEA9bsyDpWV7V8iEOQa5NGL/sJwPoLh9FofURaV5Uydbvb4uc1aZQ0T
|
||||
l1EJj1kkLN+c57cuUxj48vMcll7nSOfozSuxC3v62izzpQ+9c50rTQbLh0wqoRRs
|
||||
R62l/DYyW4TaWrZ+eB2k81VTDfrqSkWqE09Pz5HlGw0TaT4yYibLXdUCgYEAyWnl
|
||||
676IjkseusUl3IYseL2tCSxe2qnJkcMqiiLhqm4h2Nc57tP/ltxVykR967N7eEhf
|
||||
KYG0oTkaZtwSc5dwX7BwO6YlcvUZsxEwC3X5i7fNz54B9ClgMiTSen0YEODdITU8
|
||||
Awz8hZVJ18OZhOFX7R/OE34SeskT8FxC73bKXd8CgYBB8zSzD1iGtZKZ/+5r3QV6
|
||||
oUqorE3iW9Gt3KbsPIzAPE5y53dMSZ8pbFlpfJ4l4PnmgpZPPROLeM4CWYQvX/Zu
|
||||
IjzI/URNC+V0Nm49OkBl0t4K+Nk0FBwffK5gq1cGkvhbgkEdEa+0kwDQbrg0qM/F
|
||||
kPK1nc1kXjUIeOXiy9Cm5QKBgQCX4a9r0kYPZz5JARnH7bXjZsTxQfBVW+uQa2fM
|
||||
uvkJF/uWFNau7layYHyhBn0eHD5jOc/o8NJRMUpjfli7qdsnjVlU4bkjswgqLC/a
|
||||
tJ/1aClfAnversTHi1Tc4TAgqjCWNhqHAmf6I9WHSwydU4CGY5F2yX8N8EuE2U3w
|
||||
XdfEaQKBgAmfLYP9OYIrgJf9KazENSlzE06axaH9e1Td5pBWoegNQ2BscqYDqVp6
|
||||
qusnty1PU86r0kR+RmzlBNSkNKL0t9j8s5h4d8YPWJh2lQs6f/ZGXo9nGPbzLADU
|
||||
Eh7HnwpDw8AdvO07OLRYrQwToovHggqmg8ebXW56llFEudoilp8D
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user