From ce900fdbed035f65de52f14ffa6171a34553cb57 Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Fri, 5 Sep 2025 14:09:06 +0200 Subject: [PATCH] Add webhook-snippets in all languages --- webhooks-snippets/dotnet/.gitignore | 37 + .../dotnet/OpenViduMeetWebhookHandler.csproj | 9 + .../dotnet/OpenViduMeetWebhookHandler.sln | 22 + webhooks-snippets/dotnet/Program.cs | 31 + webhooks-snippets/dotnet/WebhookValidator.cs | 40 + webhooks-snippets/go/main.go | 61 +- .../io/openvidu/meet/webhooks/Controller.java | 67 +- .../meet/webhooks/WebhookValidator.java | 70 ++ webhooks-snippets/node/index.js | 22 +- webhooks-snippets/php/index.php | 60 + webhooks-snippets/php/start.sh | 4 + webhooks-snippets/python/.gitignore | 159 +++ webhooks-snippets/python/app.py | 57 + webhooks-snippets/python/requirements.txt | Bin 0 -> 30 bytes webhooks-snippets/ruby/.gitignore | 56 + webhooks-snippets/ruby/Gemfile | 4 + webhooks-snippets/ruby/app.rb | 65 ++ webhooks-snippets/rust/Cargo.lock | 1033 +++++++++++++++++ webhooks-snippets/rust/Cargo.toml | 12 + webhooks-snippets/rust/src/main.rs | 116 ++ 20 files changed, 1824 insertions(+), 101 deletions(-) create mode 100644 webhooks-snippets/dotnet/.gitignore create mode 100644 webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.csproj create mode 100644 webhooks-snippets/dotnet/OpenViduMeetWebhookHandler.sln create mode 100644 webhooks-snippets/dotnet/Program.cs create mode 100644 webhooks-snippets/dotnet/WebhookValidator.cs create mode 100644 webhooks-snippets/java/src/main/java/io/openvidu/meet/webhooks/WebhookValidator.java create mode 100644 webhooks-snippets/php/index.php create mode 100755 webhooks-snippets/php/start.sh create mode 100644 webhooks-snippets/python/.gitignore create mode 100644 webhooks-snippets/python/app.py create mode 100644 webhooks-snippets/python/requirements.txt create mode 100644 webhooks-snippets/ruby/.gitignore create mode 100644 webhooks-snippets/ruby/Gemfile create mode 100644 webhooks-snippets/ruby/app.rb create mode 100644 webhooks-snippets/rust/Cargo.lock create mode 100644 webhooks-snippets/rust/Cargo.toml create mode 100644 webhooks-snippets/rust/src/main.rs 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 0000000000000000000000000000000000000000..565d90fd4a59181b7bc51eb283f27fea437b416a GIT binary patch literal 30 icmezW&y696A(5e&A)CP#2#p!^7z}~ffPt5Riva+7iUn{0 literal 0 HcmV?d00001 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 +}