Add webhook-snippets in all languages
This commit is contained in:
parent
bd8c0a7b5d
commit
ce900fdbed
37
webhooks-snippets/dotnet/.gitignore
vendored
Normal file
37
webhooks-snippets/dotnet/.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
*.swp
|
||||
*.*~
|
||||
project.lock.json
|
||||
.DS_Store
|
||||
*.pyc
|
||||
nupkg/
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
|
||||
# Rider
|
||||
.idea
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
msbuild.log
|
||||
msbuild.err
|
||||
msbuild.wrn
|
||||
|
||||
# Visual Studio 2015
|
||||
.vs/
|
||||
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
22
webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln
Normal file
22
webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenViduMeetWebhookHandler", "OpenViduMeetWebhookHandler.csproj", "{15FDC2D1-F92F-4787-959E-06421AEEEBA0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{15FDC2D1-F92F-4787-959E-06421AEEEBA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{15FDC2D1-F92F-4787-959E-06421AEEEBA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{15FDC2D1-F92F-4787-959E-06421AEEEBA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{15FDC2D1-F92F-4787-959E-06421AEEEBA0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
31
webhooks-snippets/dotnet/Program.cs
Normal file
31
webhooks-snippets/dotnet/Program.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Text.Json;
|
||||
|
||||
const int SERVER_PORT = 5080;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapPost("/webhook", async (HttpContext context) =>
|
||||
{
|
||||
using var reader = new StreamReader(context.Request.Body);
|
||||
var bodyContent = await reader.ReadToEndAsync();
|
||||
|
||||
var headers = context.Request.Headers.ToDictionary(
|
||||
h => h.Key.ToLower(),
|
||||
h => h.Value.ToString()
|
||||
);
|
||||
|
||||
if (!WebhookValidator.IsWebhookEventValid(bodyContent, headers))
|
||||
{
|
||||
Console.WriteLine("Invalid webhook signature");
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Invalid webhook signature");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Webhook received: {bodyContent}");
|
||||
context.Response.StatusCode = 200;
|
||||
});
|
||||
|
||||
Console.WriteLine($"Webhook server listening on port {SERVER_PORT}");
|
||||
app.Run($"http://0.0.0.0:{SERVER_PORT}");
|
||||
40
webhooks-snippets/dotnet/WebhookValidator.cs
Normal file
40
webhooks-snippets/dotnet/WebhookValidator.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
public class WebhookValidator
|
||||
{
|
||||
private const long MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
|
||||
private const string OPENVIDU_MEET_API_KEY = "meet-api-key";
|
||||
|
||||
public static bool IsWebhookEventValid(string body, Dictionary<string, string> headers)
|
||||
{
|
||||
if (!headers.TryGetValue("x-signature", out var signature) ||
|
||||
!headers.TryGetValue("x-timestamp", out var timestampStr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!long.TryParse(timestampStr, out long timestamp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long current = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
long diffTime = current - timestamp;
|
||||
if (diffTime >= MAX_WEBHOOK_AGE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string signedPayload = $"{timestamp}.{body}";
|
||||
|
||||
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(OPENVIDU_MEET_API_KEY)))
|
||||
{
|
||||
byte[] expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
|
||||
byte[] actual = Convert.FromHexString(signature);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,9 +3,9 @@ package main
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -15,59 +15,62 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SERVER_PORT = "5080"
|
||||
API_KEY = "meet-api-key"
|
||||
MAX_ELAPSED_TIME = 5 * time.Minute
|
||||
serverPort = "5080"
|
||||
maxWebhookAge = 120 * 1000 // 2 minutes in milliseconds
|
||||
openviduMeetApiKey = "meet-api-key"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.POST("/webhook", handleWebhook)
|
||||
router.Run(":" + SERVER_PORT)
|
||||
router.Run(":" + serverPort)
|
||||
}
|
||||
|
||||
func handleWebhook(c *gin.Context) {
|
||||
var body map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
signature := c.GetHeader("x-signature")
|
||||
timestampStr := c.GetHeader("x-timestamp")
|
||||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid timestamp"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if !isWebhookEventValid(body, signature, timestamp) {
|
||||
if !isWebhookEventValid(bodyBytes, c.Request.Header) {
|
||||
log.Println("Invalid webhook signature")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid webhook signature"})
|
||||
return
|
||||
}
|
||||
|
||||
event, _ := json.Marshal(body)
|
||||
log.Println("Webhook received:", string(event))
|
||||
log.Println("Webhook received:", string(bodyBytes))
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func isWebhookEventValid(body map[string]interface{}, signature string, timestamp int64) bool {
|
||||
current := time.Now().UnixMilli()
|
||||
diffTime := current - timestamp
|
||||
if diffTime > MAX_ELAPSED_TIME.Milliseconds() {
|
||||
func isWebhookEventValid(bodyBytes []byte, headers http.Header) bool {
|
||||
signature := headers.Get("x-signature")
|
||||
tsStr := headers.Get("x-timestamp")
|
||||
if signature == "" || tsStr == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
timestamp, err := strconv.ParseInt(tsStr, 10, 64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
signedPayload := fmt.Sprintf("%d.%s", timestamp, string(bodyBytes))
|
||||
h := hmac.New(sha256.New, []byte(API_KEY))
|
||||
h.Write([]byte(signedPayload))
|
||||
expectedSignature := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
return hmac.Equal([]byte(expectedSignature), []byte(signature))
|
||||
current := time.Now().UnixMilli()
|
||||
diffTime := current - timestamp
|
||||
if diffTime >= maxWebhookAge {
|
||||
return false
|
||||
}
|
||||
|
||||
signedPayload := tsStr + "." + string(bodyBytes)
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(openviduMeetApiKey))
|
||||
mac.Write([]byte(signedPayload))
|
||||
expected := mac.Sum(nil)
|
||||
|
||||
actual, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return subtle.ConstantTimeCompare(expected, actual) == 1
|
||||
}
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
package io.openvidu.meet.webhooks;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
@ -18,15 +12,12 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
public class Controller {
|
||||
|
||||
private static final String API_KEY = "meet-api-key";
|
||||
private static final long MAX_ELAPSED_TIME = 5 * 60 * 1000;
|
||||
|
||||
@PostMapping("/webhook")
|
||||
public ResponseEntity<String> handleWebhook(@RequestBody String body,
|
||||
@RequestHeader(value = "x-signature") String signature,
|
||||
@RequestHeader(value = "x-timestamp") String timestampHeader) {
|
||||
public ResponseEntity<String> handleWebhook(@RequestBody String body, @RequestHeader Map<String, String> headers) {
|
||||
|
||||
if (!isWebhookEventValid(body, signature, timestampHeader)) {
|
||||
WebhookValidator validator = new WebhookValidator();
|
||||
|
||||
if (!validator.isWebhookEventValid(body, headers)) {
|
||||
System.err.println("Invalid webhook signature");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid webhook signature");
|
||||
}
|
||||
@ -34,52 +25,4 @@ public class Controller {
|
||||
System.out.println("Webhook received: " + body);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private boolean isWebhookEventValid(String body, String signature, String timestampHeader) {
|
||||
if (!StringUtils.hasText(signature) || !StringUtils.hasText(timestampHeader)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long timestamp;
|
||||
try {
|
||||
timestamp = Long.parseLong(timestampHeader);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long current = Instant.now().toEpochMilli();
|
||||
long diffTime = current - timestamp;
|
||||
if (diffTime >= MAX_ELAPSED_TIME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String signedPayload = timestamp + "." + body;
|
||||
String expectedSignature = hmacSha256(signedPayload, API_KEY);
|
||||
|
||||
return MessageDigest.isEqual(expectedSignature.getBytes(StandardCharsets.UTF_8),
|
||||
signature.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String hmacSha256(String data, String key) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to calculate HMAC SHA-256", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] hash) {
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
package io.openvidu.meet.webhooks;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class WebhookValidator {
|
||||
private static final long MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
|
||||
private static final String OPENVIDU_MEET_API_KEY = "meet-api-key";
|
||||
|
||||
public boolean isWebhookEventValid(Object body, Map<String, String> headers) {
|
||||
String signature = headers.get("x-signature");
|
||||
String ts = headers.get("x-timestamp");
|
||||
if (signature == null || ts == null) return false;
|
||||
|
||||
long timestamp;
|
||||
try {
|
||||
timestamp = Long.parseLong(ts);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long current = System.currentTimeMillis();
|
||||
long diffTime = current - timestamp;
|
||||
if (diffTime >= MAX_WEBHOOK_AGE) {
|
||||
// Webhook event too old
|
||||
return false;
|
||||
}
|
||||
|
||||
String signedPayload = timestamp + "." + body.toString();
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(
|
||||
new SecretKeySpec(
|
||||
OPENVIDU_MEET_API_KEY.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA256"
|
||||
)
|
||||
);
|
||||
byte[] expected = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] actual = hexToBytes(signature);
|
||||
|
||||
return timingSafeEqual(expected, actual);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to convert hex string to byte array
|
||||
private byte[] hexToBytes(String hex) {
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
|
||||
+ Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Time safe comparison to prevent timing attacks
|
||||
private boolean timingSafeEqual(byte[] a, byte[] b) {
|
||||
if (a.length != b.length) return false;
|
||||
int result = 0;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
result |= a[i] ^ b[i];
|
||||
}
|
||||
return result == 0;
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,8 @@ import express from "express";
|
||||
import crypto from "crypto";
|
||||
|
||||
const SERVER_PORT = 5080;
|
||||
const API_KEY = "meet-api-key";
|
||||
const MAX_ELAPSED_TIME = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
const OPENVIDU_MEET_API_KEY = "meet-api-key";
|
||||
const MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@ -22,7 +22,7 @@ app.post("/webhook", (req, res) => {
|
||||
});
|
||||
|
||||
app.listen(SERVER_PORT, () =>
|
||||
console.log("Webhook server listening on port 3000")
|
||||
console.log("Webhook server listening on port " + SERVER_PORT)
|
||||
);
|
||||
|
||||
function isWebhookEventValid(body, headers) {
|
||||
@ -35,17 +35,19 @@ function isWebhookEventValid(body, headers) {
|
||||
|
||||
const current = Date.now();
|
||||
const diffTime = current - timestamp;
|
||||
if (diffTime >= MAX_WEBHOOK_AGE) {
|
||||
// Webhook event too old
|
||||
return false;
|
||||
}
|
||||
|
||||
const signedPayload = `${timestamp}.${JSON.stringify(body)}`;
|
||||
const expectedSignature = crypto
|
||||
.createHmac("sha256", API_KEY)
|
||||
.update(signedPayload)
|
||||
.createHmac("sha256", OPENVIDU_MEET_API_KEY)
|
||||
.update(signedPayload, "utf8")
|
||||
.digest("hex");
|
||||
|
||||
return (
|
||||
crypto.timingSafeEqual(
|
||||
Buffer.from(expectedSignature, "utf-8"),
|
||||
Buffer.from(signature, "utf-8")
|
||||
) && diffTime < MAX_ELAPSED_TIME
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expectedSignature, "hex"),
|
||||
Buffer.from(signature, "hex")
|
||||
);
|
||||
}
|
||||
|
||||
60
webhooks-snippets/php/index.php
Normal file
60
webhooks-snippets/php/index.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
const MAX_WEBHOOK_AGE = 120 * 1000; // 2 minutes in milliseconds
|
||||
const OPENVIDU_MEET_API_KEY = "meet-api-key";
|
||||
|
||||
$requestPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
if ($requestMethod === 'POST' && $requestPath === '/webhook') {
|
||||
handleWebhook();
|
||||
} else {
|
||||
http_response_code(404);
|
||||
echo "Not Found\n";
|
||||
}
|
||||
|
||||
function handleWebhook()
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
|
||||
// Convert header keys to lowercase for consistent access
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
|
||||
if (!isWebhookEventValid($body, $headers)) {
|
||||
http_response_code(401);
|
||||
echo "Invalid webhook signature\n";
|
||||
return;
|
||||
}
|
||||
|
||||
http_response_code(200);
|
||||
$msg = "Webhook received: " . json_encode($body);
|
||||
echo $msg . "\n";
|
||||
error_log($msg); // Log to server console
|
||||
}
|
||||
|
||||
function isWebhookEventValid($body, $headers)
|
||||
{
|
||||
$signature = $headers['x-signature'] ?? null;
|
||||
$timestampStr = $headers['x-timestamp'] ?? null;
|
||||
if (!$signature || !$timestampStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timestamp = filter_var($timestampStr, FILTER_VALIDATE_INT);
|
||||
if ($timestamp === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current = intval(microtime(true) * 1000);
|
||||
$diffTime = $current - $timestamp;
|
||||
if ($diffTime >= MAX_WEBHOOK_AGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$signedPayload = $timestamp . '.' . json_encode($body, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$expected = hash_hmac('sha256', $signedPayload, OPENVIDU_MEET_API_KEY);
|
||||
|
||||
return hash_equals($expected, $signature);
|
||||
}
|
||||
4
webhooks-snippets/php/start.sh
Executable file
4
webhooks-snippets/php/start.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Webhook server listening on port 5080"
|
||||
php -S 0.0.0.0:5080 index.php
|
||||
159
webhooks-snippets/python/.gitignore
vendored
Normal file
159
webhooks-snippets/python/.gitignore
vendored
Normal file
@ -0,0 +1,159 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
57
webhooks-snippets/python/app.py
Normal file
57
webhooks-snippets/python/app.py
Normal file
@ -0,0 +1,57 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from flask import Flask, request
|
||||
|
||||
SERVER_PORT = 5080
|
||||
MAX_WEBHOOK_AGE = 120 * 1000 # 2 minutes in milliseconds
|
||||
OPENVIDU_MEET_API_KEY = "meet-api-key"
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/webhook", methods=["POST"])
|
||||
def webhook():
|
||||
body = request.get_json()
|
||||
headers = request.headers
|
||||
|
||||
if not is_webhook_event_valid(body, headers):
|
||||
print("Invalid webhook signature")
|
||||
return "Invalid webhook signature", 401
|
||||
|
||||
print("Webhook received:", body)
|
||||
return "", 200
|
||||
|
||||
|
||||
def is_webhook_event_valid(body, headers):
|
||||
signature = headers.get("x-signature")
|
||||
timestamp_str = headers.get("x-timestamp")
|
||||
if not signature or not timestamp_str:
|
||||
return False
|
||||
|
||||
try:
|
||||
timestamp = int(timestamp_str)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
current = int(time.time() * 1000)
|
||||
diff_time = current - timestamp
|
||||
if diff_time >= MAX_WEBHOOK_AGE:
|
||||
return False
|
||||
|
||||
json_body = json.dumps(body, separators=(",", ":"))
|
||||
signed_payload = str(timestamp) + "." + json_body
|
||||
|
||||
expected = hmac.new(
|
||||
OPENVIDU_MEET_API_KEY.encode("utf-8"),
|
||||
signed_payload.encode("utf-8"),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Webhook server listening on port " + str(SERVER_PORT))
|
||||
app.run(debug=False, host="0.0.0.0", port=SERVER_PORT)
|
||||
BIN
webhooks-snippets/python/requirements.txt
Normal file
BIN
webhooks-snippets/python/requirements.txt
Normal file
Binary file not shown.
56
webhooks-snippets/ruby/.gitignore
vendored
Normal file
56
webhooks-snippets/ruby/.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
*.gem
|
||||
*.rbc
|
||||
/.config
|
||||
/coverage/
|
||||
/InstalledFiles
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/spec/examples.txt
|
||||
/test/tmp/
|
||||
/test/version_tmp/
|
||||
/tmp/
|
||||
|
||||
# Used by dotenv library to load environment variables.
|
||||
# .env
|
||||
|
||||
# Ignore Byebug command history file.
|
||||
.byebug_history
|
||||
|
||||
## Specific to RubyMotion:
|
||||
.dat*
|
||||
.repl_history
|
||||
build/
|
||||
*.bridgesupport
|
||||
build-iPhoneOS/
|
||||
build-iPhoneSimulator/
|
||||
|
||||
## Specific to RubyMotion (use of CocoaPods):
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# vendor/Pods/
|
||||
|
||||
## Documentation cache and generated files:
|
||||
/.yardoc/
|
||||
/_yardoc/
|
||||
/doc/
|
||||
/rdoc/
|
||||
|
||||
## Environment normalization:
|
||||
/.bundle/
|
||||
/vendor/bundle
|
||||
/lib/bundler/man/
|
||||
|
||||
# for a library or gem, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
Gemfile.lock
|
||||
# .ruby-version
|
||||
# .ruby-gemset
|
||||
|
||||
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
||||
.rvmrc
|
||||
|
||||
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
||||
# .rubocop-https?--*
|
||||
4
webhooks-snippets/ruby/Gemfile
Normal file
4
webhooks-snippets/ruby/Gemfile
Normal file
@ -0,0 +1,4 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'sinatra', '~> 4.1'
|
||||
gem 'json', '~> 2.13'
|
||||
65
webhooks-snippets/ruby/app.rb
Normal file
65
webhooks-snippets/ruby/app.rb
Normal file
@ -0,0 +1,65 @@
|
||||
require 'sinatra'
|
||||
require 'json'
|
||||
require 'openssl'
|
||||
|
||||
SERVER_PORT = 5080
|
||||
MAX_WEBHOOK_AGE = 120 * 1000 # 2 minutes in milliseconds
|
||||
OPENVIDU_MEET_API_KEY = 'meet-api-key'
|
||||
|
||||
set :port, SERVER_PORT
|
||||
set :bind, '0.0.0.0'
|
||||
set :environment, :production
|
||||
disable :protection
|
||||
|
||||
post '/webhook' do
|
||||
body_content = request.body.read
|
||||
|
||||
begin
|
||||
body = JSON.parse(body_content)
|
||||
rescue JSON::ParserError
|
||||
status 400
|
||||
return 'Invalid JSON'
|
||||
end
|
||||
|
||||
headers = {}
|
||||
request.env.each do |key, value|
|
||||
if key.start_with?('HTTP_')
|
||||
header_name = key[5..-1].downcase.gsub('_', '-')
|
||||
headers[header_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
unless webhook_event_valid?(body, headers)
|
||||
puts 'Invalid webhook signature'
|
||||
status 401
|
||||
return 'Invalid webhook signature'
|
||||
end
|
||||
|
||||
puts "Webhook received: #{body_content}"
|
||||
status 200
|
||||
''
|
||||
end
|
||||
|
||||
def webhook_event_valid?(body, headers)
|
||||
signature = headers['x-signature']
|
||||
timestamp_str = headers['x-timestamp']
|
||||
return false if signature.nil? || timestamp_str.nil?
|
||||
|
||||
begin
|
||||
timestamp = Integer(timestamp_str)
|
||||
rescue ArgumentError
|
||||
return false
|
||||
end
|
||||
|
||||
current = (Time.now.to_f * 1000).to_i
|
||||
diff_time = current - timestamp
|
||||
return false if diff_time >= MAX_WEBHOOK_AGE
|
||||
|
||||
signed_payload = "#{timestamp}.#{body.to_json}"
|
||||
|
||||
expected = OpenSSL::HMAC.hexdigest('SHA256', OPENVIDU_MEET_API_KEY, signed_payload)
|
||||
|
||||
OpenSSL.fixed_length_secure_compare(expected, signature)
|
||||
end
|
||||
|
||||
puts "Webhook server listening on port #{SERVER_PORT}"
|
||||
1033
webhooks-snippets/rust/Cargo.lock
generated
Normal file
1033
webhooks-snippets/rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
webhooks-snippets/rust/Cargo.toml
Normal file
12
webhooks-snippets/rust/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "openvidu-meet-webhook-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8.4"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
chrono = "0.4"
|
||||
116
webhooks-snippets/rust/src/main.rs
Normal file
116
webhooks-snippets/rust/src/main.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use axum::{
|
||||
extract::Request,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Response,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
const SERVER_PORT: u16 = 5080;
|
||||
const MAX_WEBHOOK_AGE: i64 = 120 * 1000; // 2 minutes in milliseconds
|
||||
const OPENVIDU_MEET_API_KEY: &str = "meet-api-key";
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("Webhook server listening on port {}", SERVER_PORT);
|
||||
|
||||
let app = Router::new().route("/webhook", post(webhook_handler));
|
||||
|
||||
let listener = TcpListener::bind(format!("0.0.0.0:{}", SERVER_PORT))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
async fn webhook_handler(
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
) -> Result<Response<String>, StatusCode> {
|
||||
let body = match axum::body::to_bytes(request.into_body(), usize::MAX).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return Err(StatusCode::BAD_REQUEST),
|
||||
};
|
||||
|
||||
let body_str = match std::str::from_utf8(&body) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(StatusCode::BAD_REQUEST),
|
||||
};
|
||||
|
||||
// Extract headers
|
||||
let mut header_map = HashMap::new();
|
||||
for (key, value) in headers.iter() {
|
||||
if let Ok(value_str) = value.to_str() {
|
||||
header_map.insert(key.as_str().to_lowercase(), value_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !is_webhook_event_valid(body_str, &header_map) {
|
||||
println!("Invalid webhook signature");
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.body("Invalid webhook signature".to_string())
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
println!("Webhook received: {}", body_str);
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body("".to_string())
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
fn is_webhook_event_valid(body_str: &str, headers: &HashMap<String, String>) -> bool {
|
||||
let signature = match headers.get("x-signature") {
|
||||
Some(sig) => sig,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let timestamp_str = match headers.get("x-timestamp") {
|
||||
Some(ts) => ts,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let timestamp: i64 = match timestamp_str.parse() {
|
||||
Ok(ts) => ts,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
// Check timestamp age
|
||||
let current = Utc::now().timestamp_millis();
|
||||
let diff_time = current - timestamp;
|
||||
if diff_time >= MAX_WEBHOOK_AGE {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create signed payload using the raw body string
|
||||
let signed_payload = format!("{}.{}", timestamp, body_str);
|
||||
|
||||
// Calculate HMAC
|
||||
let mut mac = match HmacSha256::new_from_slice(OPENVIDU_MEET_API_KEY.as_bytes()) {
|
||||
Ok(mac) => mac,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
mac.update(signed_payload.as_bytes());
|
||||
let expected = mac.finalize().into_bytes();
|
||||
let expected_hex = hex::encode(expected);
|
||||
|
||||
// Timing-safe comparison
|
||||
if signature.len() != expected_hex.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut result = 0u8;
|
||||
for (a, b) in signature.bytes().zip(expected_hex.bytes()) {
|
||||
result |= a ^ b;
|
||||
}
|
||||
result == 0
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user