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