diff --git a/webhooks-snippets/dotnet/.gitignore b/webhooks-snippets/dotnet/.gitignore
new file mode 100644
index 0000000..0626272
--- /dev/null
+++ b/webhooks-snippets/dotnet/.gitignore
@@ -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/
\ No newline at end of file
diff --git a/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.csproj b/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.csproj
new file mode 100644
index 0000000..1b28a01
--- /dev/null
+++ b/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln b/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln
new file mode 100644
index 0000000..4449eb4
--- /dev/null
+++ b/webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln
@@ -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
diff --git a/webhooks-snippets/dotnet/Program.cs b/webhooks-snippets/dotnet/Program.cs
new file mode 100644
index 0000000..bcd1824
--- /dev/null
+++ b/webhooks-snippets/dotnet/Program.cs
@@ -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}");
diff --git a/webhooks-snippets/dotnet/WebhookValidator.cs b/webhooks-snippets/dotnet/WebhookValidator.cs
new file mode 100644
index 0000000..1af9431
--- /dev/null
+++ b/webhooks-snippets/dotnet/WebhookValidator.cs
@@ -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 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/webhooks-snippets/go/main.go b/webhooks-snippets/go/main.go
index 1320927..a80e3e8 100644
--- a/webhooks-snippets/go/main.go
+++ b/webhooks-snippets/go/main.go
@@ -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))
+ current := time.Now().UnixMilli()
+ diffTime := current - timestamp
+ if diffTime >= maxWebhookAge {
+ return false
+ }
- return hmac.Equal([]byte(expectedSignature), []byte(signature))
+ 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
}
diff --git a/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/Controller.java b/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/Controller.java
index 1273ef7..65fa8c9 100644
--- a/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/Controller.java
+++ b/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/Controller.java
@@ -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 handleWebhook(@RequestBody String body,
- @RequestHeader(value = "x-signature") String signature,
- @RequestHeader(value = "x-timestamp") String timestampHeader) {
+ public ResponseEntity handleWebhook(@RequestBody String body, @RequestHeader Map 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();
- }
}
diff --git a/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/WebhookValidator.java b/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/WebhookValidator.java
new file mode 100644
index 0000000..09897cb
--- /dev/null
+++ b/webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/WebhookValidator.java
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/webhooks-snippets/node/index.js b/webhooks-snippets/node/index.js
index d4b142a..e0ba1b1 100644
--- a/webhooks-snippets/node/index.js
+++ b/webhooks-snippets/node/index.js
@@ -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")
);
}
diff --git a/webhooks-snippets/php/index.php b/webhooks-snippets/php/index.php
new file mode 100644
index 0000000..c84a3ec
--- /dev/null
+++ b/webhooks-snippets/php/index.php
@@ -0,0 +1,60 @@
+= 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);
+}
diff --git a/webhooks-snippets/php/start.sh b/webhooks-snippets/php/start.sh
new file mode 100755
index 0000000..538fa8e
--- /dev/null
+++ b/webhooks-snippets/php/start.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+echo "Webhook server listening on port 5080"
+php -S 0.0.0.0:5080 index.php
diff --git a/webhooks-snippets/python/.gitignore b/webhooks-snippets/python/.gitignore
new file mode 100644
index 0000000..4b3780e
--- /dev/null
+++ b/webhooks-snippets/python/.gitignore
@@ -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/
\ No newline at end of file
diff --git a/webhooks-snippets/python/app.py b/webhooks-snippets/python/app.py
new file mode 100644
index 0000000..fdf38e7
--- /dev/null
+++ b/webhooks-snippets/python/app.py
@@ -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)
diff --git a/webhooks-snippets/python/requirements.txt b/webhooks-snippets/python/requirements.txt
new file mode 100644
index 0000000..565d90f
Binary files /dev/null and b/webhooks-snippets/python/requirements.txt differ
diff --git a/webhooks-snippets/ruby/.gitignore b/webhooks-snippets/ruby/.gitignore
new file mode 100644
index 0000000..c7ed8e7
--- /dev/null
+++ b/webhooks-snippets/ruby/.gitignore
@@ -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?--*
\ No newline at end of file
diff --git a/webhooks-snippets/ruby/Gemfile b/webhooks-snippets/ruby/Gemfile
new file mode 100644
index 0000000..d6a050d
--- /dev/null
+++ b/webhooks-snippets/ruby/Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem 'sinatra', '~> 4.1'
+gem 'json', '~> 2.13'
diff --git a/webhooks-snippets/ruby/app.rb b/webhooks-snippets/ruby/app.rb
new file mode 100644
index 0000000..98a9c09
--- /dev/null
+++ b/webhooks-snippets/ruby/app.rb
@@ -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}"
diff --git a/webhooks-snippets/rust/Cargo.lock b/webhooks-snippets/rust/Cargo.lock
new file mode 100644
index 0000000..8553091
--- /dev/null
+++ b/webhooks-snippets/rust/Cargo.lock
@@ -0,0 +1,1033 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.175"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "openvidu-meet-webhook-server"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "chrono",
+ "hex",
+ "hmac",
+ "sha2",
+ "tokio",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/webhooks-snippets/rust/Cargo.toml b/webhooks-snippets/rust/Cargo.toml
new file mode 100644
index 0000000..b44f27b
--- /dev/null
+++ b/webhooks-snippets/rust/Cargo.toml
@@ -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"
diff --git a/webhooks-snippets/rust/src/main.rs b/webhooks-snippets/rust/src/main.rs
new file mode 100644
index 0000000..b661edc
--- /dev/null
+++ b/webhooks-snippets/rust/src/main.rs
@@ -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;
+
+#[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, 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) -> 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
+}