WebHook behaviors provide webHook functionality in the context of Cloud Director. In short, upon invocation a webHook behavior a POST request is sent to a webHook server. After receiving the server’s response, the behavior invocation task is updated accordingly. The payload sent to the webHook server is configurable.
A webHook server must be set-up to process requests from webHook behavior invocations before creating any webHook behaviors. The webHook server must support HTTPs and its SSL certificate must be trusted in the organization of the user invoking the webHook behavior in order for the connection to be successful (how to trust a certificate in Cloud Director).
For testing purposes you can use webhooks.site. It provides a random URL which canbe used as a webHook server URL. The response sent back from webhooks.site to each request can be customized.
WebHook behavior example definition:
{
"name": "webhookBehavior",
"execution": {
"type": "WebHook",
"id": "testWebHook",
"href": "https://example.com/webhooks" ,
"_internal_key": "verySecretKey",
"execution_properties": {
"template": {
"content": "<#assign header_Authorization = \"${_execution_properties._secure_token}\" />{\"text\": \"Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}\" }"
},
"_secure_token": "secureToken"
}
}
}
The webHook behavior’s execution type
is WebHook
. It is a required field.
The id
is a user-defined string. It is a required field.
The href
holds the URL of the webHook server which will be processing the webHook requests. The value must be a valid URL of https
protocol. It is a required field.
The _internal_key
holds a shared secret string which is only known to Cloud Director and the webHook server. This shared secret will be used to generate the signature header of the request which is part of the HMAC authentication. It is a required field.
The template
field in the execution can be used to set-up a custom payload for the requests sent to the webHook server. More information can be found here.
The default payload sent to webHook servers is a JSON containing information about the actual behavior invocation (behavior information, entity information, and some metadata). However, you can also customize this payload by setting a template in the behavior definition. FreeMarker template engine is used to render the payload from the provided template and the data model.
The default payload sent to webHook servers is a JSON containing information about the actual behavior invocation - invocation arguments, behavior information, entity information, and some metadata.
Default headers:
content-type=[application/json],
x-vcloud-signature=[ algorithm="hmac-sha512", headers="host date (request-target) digest", signature="WjuxZuVgNXctRBuDLONeQ0NWXt+O36YL8wMdjhCGCeW7Fq8sMHfU6NCS0O6STJx2z/wRkHTjzil4GAfuho9ZUw=="],
x-vcloud-digest=[SHA-512=Yt4eiT2VmUyX8wDt6wneZ10VRk1B1H2jmHP1R7YanI9hykEAjUdtg7JzxfioBQm/iWRRNBM8B0aJnw6Jd29Jqg==],
date=[Thu, 01 Oct 2020 12:57:31 GMT],
accept=[*/*],
user-agent=[Apache-CXF/3.1.17],
cache-control=[no-cache],
pragma=[no-cache],
host=[127.0.0.1:1234],
connection=[keep-alive],
content-length=[579]
Default payload:
{
"_execution_properties": {
"actAsToken": true
},
"entity.masked": {
"entity": {
"VcdVm": {
"name": true
}
}
},
"entityId": "urn:vcloud:entity:vmware:testType:28b0488f-39f0-49d6-a78b-c37e8eaf40be",
"typeId": "urn:vcloud:type:vmware:testType:1.0.1",
"arguments": {
"x": 7
},
"_metadata": {
"executionId": "testWebHook",
"execution": {
"href": "https://webhook.site/f17d3903-5f89-468b-b4b0-40a63795bb8b"
},
"invocation": {
"y": 6
},
"apiVersion": "37.3",
"behaviorId": "urn:vcloud:behavior-interface:webhookBehavor2:vmware:test2:1.0.0",
"requestId": "4551c4ee-27df-47dc-b065-b67a91d3904c",
"executionType": "WebHook",
"actAsToken": "...",
"invocationId": "b2031c44-c426-4eee-bae1-423bf5bb4e92",
"taskId": "c928b926-7c1d-40bd-ae0e-2b541e0d5511"
},
"entity": {
"entity": {
"VcdVm": {
"name": true
}
}
}
}
See the Java Class representing the InvocationArguments in the Code Examples
section.
The payload can be customized by setting a template in the behavior definition. FreeMarker template engine is used to render the payload from the provided template and the data model.
The data model represents all the data that was prepared for the template. Or in other words, all the data which can be included in the payload. As far as the template author is concerned, the data model is a tree-like structure and can be visualized as:
+ - entityId
|
+ - typeId
|
+ - arguments
|
+ - arguments_string
|
+ - _execution_properties
|
+ - _metadata
| |
| + - executionId
| + - behaviorId
| + - executionType
| + - taskId
| + - execution // can select all the values from execution by key name
| | |
| | + - href
| + - invocation // can select all the values from invocation by key name
| + - invocationId
| + - requestId
| + - apiVersion
|
+ - entity
|
+ - entity_string
entityId
- the ID of the defined entity which the behavior was invoked on.typeId
- the ID of the entity type of the defined entity which the behavior was invoked on.arguments
- the user supplied arguments from the body of the behavior invocation.arguments_string
- a JSON-encoded string of arguments, can be used if the user wants to add all the arguments to the payload._execution_properties
- the execution_properties
from the behavior definition.behaviorId
- the ID of the invoked behavior.executionType
- the execution type of the invoked behavior.taskId
- the ID of the behavior invocation task.execution
- the execution from the behavior definition.invocation
- the user supplied metadata from the body of the behavior invocation.invocationId
- the ID of the behavior invocation.apiVersion
- the API version the behavior was invoked with.entity
- a map of the entity contents, can select all values in entity by key name.entity_string
- a JSON-encoded string of the entity contents.More details on how to build a template can be found in the FreeMarker documentation . In short, the way to evaluate a key from the data model is wrapping the key in ${…}
. For example ${_metadata.execution.href}
is the way to get the href
value from the execution. An example template is:
{
"text": "Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}"
}
Custom headers can be set in the webHook reques by using the template. Each header can be set as a variable in the template with the prefix header_
.
Examples:
<#assign header_Content-Type = "application/json" />
<#assign header_Authorization = "${_execution_properties._secure_token}" />
Example behavior definition with custom payload and headers:
{
"name": "test-behavior",
"execution": {
"type": "WebHook",
"id": "testWebHook",
"href": "https://webhook.site/45b2d281-9de8-4eec-b7cc-a67529ec4be4",
"_internal_key": "secretKey",
"execution_properties": {
"template": {
"content": "<#assign header_Authorization = \"${_execution_properties._secure_token}\" />{\"text\": \"Behavior with id ${_metadata.behaviorId} was executed on entity with id ${entityId}\"}"
},
"_secure_token": "secureToken"
}
}
}
There are three possible response formats for the webHook server to send back to Cloud Director. Each one is processed differently by Cloud Director.
Sets the behavior invocation task result only.
success
and the response body is used as the task resultStatus code: 200
Content-Type: text/plain //or none
...some string...
ERROR
response code != 200
Allows for updating not only the behavior invocation task result
, but also task’s status
, details
, operation
, error
, progress
. The payload must represent a valid JSON representation of a TaskType
with the task properties that need to be modified.
Status code: 200
Content-Type: application/vnd.vmware.vcloud.task+json
{
"status": "success",
"details": "example details",
"operation": "example operation",
"progress": 100,
"result": {
"resultContent": "example result"
}
}
Status code: 200
Content-Type: application/vnd.vmware.vcloud.task+json
{
"status": "error",
"details": "example details",
"operation": "example operation",
"progress": 50,
"error": {
"majorErrorCode": 404,
"minorErrorCode": "ERROR",
"message": "example error message"
}
}
Allows for sending multiple task updates before completing the behavior invocation task. This is done by using HTTP multipart response. Each update is a separate body part of the response body. The last body part of the response body must complete the task. If it does not, the task is completed with an error message indicating that the task should have been completed but was not.
Example response:
Status code: 200
Content-Type: multipart/form-data; boundary=<boundary_string>
--<boundary_string>
Content-Type: application/vnd.vmware.vcloud.task+json
{
"details": "example details",
"operation": "example operation",
"progress": 50
}
--<boundary_string>
Content-Type: application/vnd.vmware.vcloud.task+json
{
"status": "success",
"progress": 100,
"result": {
"resultContent": "example result"
}
}
--<boundary_string>
There is a new line after each boundary string
, Content-Type
header and body. The body parts of the response can either represent a simple response or a task update response. The last body part of the webHook server response body must complete the task. If it does not, the task is completed with an error message indicating that the task should have been completed but was not. Once a body part completes the behavior invocation task, any other body parts received after that are ignored.
See the Java class representing a Task in the Code Examples
section.
WebHook behavior requests are secured via HMAC authentication.
HMAC (or hash-based message authentication code) is a mechanism used to ensure the authenticity and integrity of HTTP requests. This is achieved by including two custom headers to each HTTP request – signature header and digest (in our case x-vcloud-signature
and x-vcloud-digest
).
x-vcloud-signature=[ algorithm="hmac-sha512", headers="host date (request-target) digest", signature="WjuxZuVgNXctRBuDLONeQ0NWXt+O36YL8wMdjhCGCeW7Fq8sMHfU6NCS0O6STJx2z/wRkHTjzil4GAfuho9ZUw=="]
x-vcloud-digest=[SHA-512=Yt4eiT2VmUyX8wDt6wneZ10VRk1B1H2jmHP1R7YanI9hykEAjUdtg7JzxfioBQm/iWRRNBM8B0aJnw6Jd29Jqg==]
_internal_key
). This shared secret will be used to generate the signature headerx-vcloud-signature
) - This is a custom header sent with each webHook request. It consists of three parts:
algorithm
(hmac-sha512
) - this is the algorithm used to generate the signatureheaders
(host date (request-target) digest
) - this shows what is included in the base string which is signed to create the signaturehost
- the webHook server hostdate
- date of the request(reguest-target) = <http-method> + <ASCII space> + <path>
(e.g. post /webhook
)digest
- sha-512 digest of the request bodysignature
- this is the generated signaturex-vcloud-digest
) - this is a base64
encoded sha-512 digest of the request bodyEach webHook request from Cloud Director can be verified by generating the signature using the shared secret and comparing it to the signature in the signature header from Cloud Director. If they match, the request is verified.
See the Example Java code for verifying a webHook behavior request’s signature header in the Code Examples
section.
Slack has a functionality for creating incoming webHooks which can be used for posting messages to Slack channels. WebHook behaviors can be used to integrate Slack incoming webHooks with Cloud Director, allowing Cloud Director to post messages directly to a slack channel. More information on how to create a Slack incoming webHook for a channel can be found here.
Once a Slack webHook is configured, messages to it can be sent via a POST request to the webHook URL - https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
. The request payload has to be of a specific format in order for it to be recognized by the Slack webHook and the Content-Type
header must be set to application/json
.
In order to use Cloud Director’s webHook behaviors to send messages to slack webHooks, the following steps must be followed:
Create an Interface
POST /cloudapi/1.0.0/interfaces
{
"name": "test",
"vendor": "vmware",
"nss": "test",
"version": "1.0.0"
}
Response:
{
"name": "test",
"id": "urn:vcloud:interface:vmware:test:1.0.0",
"version": "1.0.0",
"vendor": "vmware",
"nss": "test",
"readonly": false
}
Create a template for the payload sent to the webHook server
<#assign header_Content\-Type= "application/json" />
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}"
}
}
]
}
Create a webHook behavior in the test interface
For the behavior’s href
use the slack webHook URL.
POST /cloudapi/1.0.0/interfaces/urn:vcloud:interface:vmware:test:1.0.0/behaviors
{
"name": "testTemplateWebhookBehaviorSlack",
"execution": {
"type": "WebHook",
"id": "testWebHook",
"href": "https://hooks.slack.com:443/services/T07UZFN0N/B01EW5NC42D/rfjhHCGIwzuzQFrpPZiuLkIX" ,
"key": "secretKey",
"execution_properties": {
"template": {
"content": " \n \"blocks\": [\n {\n \"type\": \"section\",\n \"text\": {\n\"type\": \"mrkdwn\",\n\"text\": \"*BEHAVIOR_EXECUTION*:vcda:\\n Behavior with id \\n_${_metadata.behaviorId}_\\n was executed on entity with id \\n_${entityId}_\\n ${arguments.greeting}\"\n}\n }\n ]\n}"
}
}
}
}
Response:
{
"name": "testTemplateWebhookBehaviorSlack",
"id": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0",
"ref": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0",
"description": null,
"execution": {
"id": "testWebHook",
"href": "https://hooks.slack.com:443/services/T07UZFN0N/B01EW5NC42D/rfjhHCGIwzuzQFrpPZiuLkIX",
"type": "WebHook",
"key": "secretKey",
"execution_properties": {
"template": {
"content": "<#assign header_Content-Type= \"application/json\" /> { \"blocks\": [ { \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \"*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}\" } } ] }"
}
}
}
}
The template configures the payload to be alligned with what the Slack server expects to receive. The request Content-Type
header is set to application/json
. The body of the request is formatted using Slack webHooks formatting.
<#assign header_Content\-Type= "application/json" />
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*BEHAVIOR_EXECUTION*:vcda:\n Behavior with id \n_${_metadata.behaviorId}_\n was executed on entity with id \n_${entityId}_\n ${arguments.greeting}"
}
}
]
}
Trust Slack server SSL certificate
In order to be able to send data to Slack, the Slack server SSL certificate must be trusted. More information on managing SSL certificates in Cloud Director can be found here.
Create a RDE Type implementing the newly created interface
POST /cloudapi/1.0.0/entityTypes
{
"name": "testType",
"description": "string",
"nss": "testType",
"version": "1.0.0",
"externalId": "123",
"schema": {
"type": "object",
"properties": {
"application/json": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
},
"interfaces": ["urn:vcloud:interface:vmware:test:1.0.0"],
"vendor": "vmware",
"readonly": false
}
Response:
{
"id": "urn:vcloud:type:vmware:testType:1.0.0",
"name": "testType",
"description": "string",
"nss": "testType",
"version": "1.0.0",
"inheritedVersion": null,
"externalId": "123",
"schema": {
"type": "object",
"properties": {
"application/json": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
}
}
},
"vendor": "vmware",
"interfaces": [
"urn:vcloud:interface:vmware:test:1.0.0"
],
"hooks": null,
"readonly": false,
"maxImplicitRight": null
}
Create a Behavior Access Control for the webHook behavior
The webHook behavior invocations will be limited to users who have Full Control access to the defined entity the behavior will be invoked on.
POST /cloudapi/1.0.0/entityTypes/urn:vcloud:type:vmware:testType:1.0.0/behaviorAccessControls
{
"behaviorId": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0",
"accessLevelId": "urn:vcloud:accessLevel:FullControl"
}
Response:
{
"behaviorId": "urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0",
"accessLevelId": "urn:vcloud:accessLevel:FullControl"
}
Create an entity of the testType
RDE type
POST /cloudapi/1.0.0/entityTypes/urn:vcloud:type:vmware:testType:1.0.0
{
"name": "testEntity",
"externalId": null,
"entity": {
"application/json": {
"name": "test"
}
}
}
Response:
Headers:
Location: https://localhost:8443/api/task/9f1fe9a7-dd5e-4975-a6eb-06502359e2e4
To obtain the newly created entity’s ID you must retrieve the associated createDefinedEntity
task from the response headers. The owner
property holds the entity’s ID:
{
...
"owner": {
"otherAttributes": {},
"href": "",
"id": "urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e",
"type": "application/json",
"name": "entity",
"vCloudExtension": []
},
...
}
Resolve newly created entity
POST /cloudapi/1.0.0/entities/urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e/resolve
Response:
{
"id": "urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e",
"entity": {
"application/json": {
"name": "test"
}
},
"state": "RESOLVED",
"entityState": "RESOLVED",
"message": null
}
Invoke webHook behavior
POST /cloudapi/1.0.0/entities/urn:vcloud:entity:vmware:testType:14f02e11-d8e1-4c23-8cd9-8fa256ed9b8e/behaviors/urn:vcloud:behavior-interface:testTemplateWebhookBehaviorSlack:vmware:test:1.0.0/invocations
{
"arguments": {
"greeting": "Greetings from vCloudDirector"
}
}
The following message is posted in Slack as a result:
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
public class InvocationArguments {
private String entityId;
private String typeId;
@JsonProperty("_metadata")
private InvocationArgumentsMetadata metadata;
private Map<String, Object> entity;
private Map<String, Object> arguments;
@JsonProperty("_execution_properties")
private Map<String, Object> executionProperties;
private Map<String, Object> additionalArguments;
/**
* @return the id of the RDE which the behavior was invoked on
*/
public String getEntityId() {
return entityId;
}
public void setEntityId(String entityId) {
this.entityId = entityId;
}
/**
* @return The id of the RDE Type of the entity which the behavior was invoked in
*/
public String getTypeId() {
return typeId;
}
public void setTypeId(String typeId) {
this.typeId = typeId;
}
/**
* @return The invocation {@link InvocationArgumentsMetadata}
*/
public InvocationArgumentsMetadata getMetadata() {
return metadata;
}
public void setMetadata(InvocationArgumentsMetadata metadata) {
this.metadata = metadata;
}
/**
* @return the entity contents of the RDE which the behavior was invoked on
*/
public Map<String, Object> getEntity() {
return entity;
}
public void setEntity(Map<String, Object> entity) {
this.entity = entity;
}
/**
* @return the user-provided arguments upon invocation
*/
public Map<String, Object> getArguments() {
return arguments;
}
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
/**
* @return the behavior's execution_properties
*/
public Map<String, Object> getExecutionProperties() {
return executionProperties;
}
public void setExecutionProperties(Map<String, Object> executionProperties) {
this.executionProperties = executionProperties;
}
/**
* @return additional_arguments from the behavior's execution
*/
public Map<String, Object> getAdditionalArguments() {
return additionalArguments;
}
public void setAdditionalArguments(Map<String, Object> additionalArguments) {
this.additionalArguments = additionalArguments;
}
/**
* The behavior invocation metadata
*/
public static class InvocationArgumentsMetadata {
private String behaviorId;
private String taskId;
private String executionId;
private String executionType;
private String actAsToken;
private Map<String, Object> invocation;
/**
* @return the id of the invoked behavior
*/
public String getBehaviorId() {
return behaviorId;
}
public void setBehaviorId(String behaviorId) {
this.behaviorId = behaviorId;
}
/**
* @return the id of the behavior invocation task
*/
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
/**
* @return the behavior's execution id
*/
public String getExecutionId() {
return executionId;
}
public void setExecutionId(String executionId) {
this.executionId = executionId;
}
/**
* @return the behavior's execution type
*/
public String getExecutionType() {
return executionType;
}
public void setExecutionType(String executionType) {
this.executionType = executionType;
}
/**
* @return a {@link Map} of the user-provided metadata upon invocation
*/
public Map<String, Object> getInvocation() {
return invocation;
}
public void setInvocation(Map<String, Object> invocation) {
this.invocation = invocation;
}
/**
* @return an act-as token if additional API calls to VCD need to be made
* (it os only populated if it is specified in the behavior's definition)
*/
public String getActAsToken() {
return actAsToken;
}
public void setActAsToken(String actAsToken) {
this.actAsToken = actAsToken;
}
}
}
import com.fasterxml.jackson.annotation.JsonValue;
public class TaskType {
public static enum TaskStatus {
PENDING("pending"),
PRE_RUNNING("pre-running"),
RUNNING("running"),
SUCCESS("success"),
ABORTED("aborted"),
ERROR("error"),
CANCELED("canceled"),
EXPECTING_ACTION("expectingAction");
private final String value;
TaskStatus(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
}
public static class ErrorType {
private String majorErrorCode;
private String minorErrorCode;
private String message;
public String getMajorErrorCode() {
return majorErrorCode;
}
public void setMajorErrorCode(String majorErrorCode) {
this.majorErrorCode = majorErrorCode;
}
public String getMinorErrorCode() {
return minorErrorCode;
}
public void setMinorErrorCode(String minorErrorCode) {
this.minorErrorCode = minorErrorCode;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
private TaskType status;
private String operation;
private String details;
private ErrorType error;
private int progress;
/**
* @return the task status
*/
public TaskType getStatus() {
return status;
}
public void setStatus(TaskType status) {
this.status = status;
}
/**
* @return the task operation
*/
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
/**
* @return the task details
*/
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
/**
* @return the task error as {@link ErrorType}
*/
public ErrorType getError() {
return error;
}
public void setError(ErrorType error) {
this.error = error;
}
/**
* @return the task progress. Must be in the range [0,100].
*/
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
}
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Verifies an incoming webHook behavior request from VCD.
*/
public class HMACRequestVerificator {
private static final String DIGEST_ALGORITHM = "hmac-sha512";
private static final String HEADER_SIGNATURE = "x-vcloud-signature";
private static final String HEADER_DATE = "date";
private static final String SIGNATURE_PARAM_SIGNATURE = "signature";
private static final String SIGNATURE_PARAM_ALGORITHM = "algorithm";
private static final String SIGNATURE_PARAM_HEADERS = "headers";
private static final String HEADERS_DATE = "date";
private static final String HEADERS_HOST = "host";
private static final String HEADERS_REQUEST_TARGET = "(request-target)";
private static final String HEADERS_DIGEST = "digest";
private static final Pattern HEADER_SIGNATURE_PATTERN = Pattern.compile("^algorithm=\".+\",headers=\".+\",signature=\".+\"$");
private Map<String, List<String>> headers = null;
private Map<String, String> signatureParams = null;
private final String sharedSecret;
private String host;
private String path;
private String payload;
private static final ObjectMapper objectMapper = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL);
/**
* @param sharedSecret the shared secret which will be used to verify the signature from the VCD request headers.
*/
public HMACRequestVerificator(String sharedSecret) {
this.sharedSecret = sharedSecret;
}
/**
* Provides the host and path of the webhook server.
* @param webhookServerUrl URL of the webhook server endpoint
* @return this {@link HMACRequestVerificator}
*/
public HMACRequestVerificator withWebhookServerUrl(String webhookServerUrl) {
Optional<URI> webhookServerUri = buildWebhookServerURI(webhookServerUrl);
if (webhookServerUri.isPresent()) {
URI uri = webhookServerUri.get();
host = uri.getHost();
path = uri.getPath();
}
return this;
}
/**
* Provides the host and path of the webhook server.
* @param host the host of the webhook server
* @param path the path of the webhook server
* @return this {@link HMACRequestVerificator}
*/
public HMACRequestVerificator withWebhookServerHostAndPath(String host, String path) {
this.host = host;
this.path = path;
return this;
}
/**
* @param headers the headers of the incoming request from VCD
* @return this {@link HMACRequestVerificator}
*/
public HMACRequestVerificator withHeaders(Map<String, List<String>> headers) {
this.headers = headers;
return this;
}
/**
* @param payload the payload of the incoming request from VCD
* @return this {@link HMACRequestVerificator}
*/
public HMACRequestVerificator withPayload(Object payload) {
this.payload = serializePayload(payload);
return this;
}
/**
* Executes verification.
* @return true if verification was successful and false otherwise
*/
public boolean verify() {
if (!checkParameters()) {
return false;
}
String signatureFromServer = signatureParams.get(SIGNATURE_PARAM_SIGNATURE);
Optional<String> signatureToVerify = buildSignatureToVerify(payload);
return signatureToVerify.isPresent() && signatureFromServer.equals(signatureToVerify.get());
}
private boolean checkParameters() {
return validateParameters() && validateHeaders();
}
private boolean validateParameters() {
return sharedSecret != null && payload != null && host != null && path != null;
}
private boolean validateHeaders() {
if (headers == null
|| !headers.containsKey(HEADERS_DATE)
|| !headers.containsKey(HEADER_SIGNATURE)) {
return false;
}
String signatureHeader = headers.get(HEADER_SIGNATURE).get(0);
Matcher matcher = HEADER_SIGNATURE_PATTERN.matcher(signatureHeader);
if (!matcher.matches()) {
return false;
}
signatureParams = extractSignatureParams(headers.get(HEADER_SIGNATURE).get(0));
return signatureParams.containsKey(SIGNATURE_PARAM_SIGNATURE)
&& signatureParams.containsKey(SIGNATURE_PARAM_HEADERS)
&& signatureParams.containsKey(SIGNATURE_PARAM_ALGORITHM);
}
private Map<String, String> extractSignatureParams(String signatureHeader) {
String [] signatureHeaders = signatureHeader.split(",");
Map<String, String> result = new HashMap<>();
for (String h : signatureHeaders) {
String param = h.split("=")[0];
String value = h.split("\"")[1];
result.put(param.toLowerCase(), value);
}
return result;
}
private byte[] getDigest(String payload) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
digest.update(payload.getBytes());
return digest.digest();
} catch (NoSuchAlgorithmException e) {
//should not happen
throw new RuntimeException(e);
}
}
private Optional<String> buildSignatureToVerify(String payload) {
String signatureString = buildSignatureString(payload);
if (signatureParams.get(SIGNATURE_PARAM_ALGORITHM).equalsIgnoreCase(DIGEST_ALGORITHM)) {
return Optional.of(signDataSHA512HMAC(signatureString, sharedSecret));
}
return Optional.empty();
}
private String signDataSHA512HMAC(String data, String sharedSecret) {
try {
final byte[] byteKey = sharedSecret.getBytes(StandardCharsets.UTF_8);
Mac sha512Hmac = Mac.getInstance("HmacSHA512");
SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA512");
sha512Hmac.init(keySpec);
byte[] macData = sha512Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return getBase64(macData);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e.getMessage());
}
}
private String buildSignatureString(String payload) {
String[] headersInSignature = signatureParams.get(SIGNATURE_PARAM_HEADERS).split(" ");
return Arrays.stream(headersInSignature)
.map(headerInSignature -> buildSignatureStringHeaderParam(headerInSignature, payload))
.filter(Objects::nonNull)
.collect(Collectors.joining("\n"));
}
private String buildSignatureStringHeaderParam(String paramName, String payload) {
if (paramName.equalsIgnoreCase(HEADERS_HOST)) {
return "host: " + host;
} else if (paramName.equalsIgnoreCase(HEADERS_DATE)) {
return "date: " + getDate();
} else if (paramName.equalsIgnoreCase(HEADERS_DIGEST)) {
return "digest: SHA-512=" + getBase64(getDigest(payload));
} else if (paramName.equalsIgnoreCase(HEADERS_REQUEST_TARGET)) {
return "(request-target): post " + path;
}
return null;
}
private String getBase64(byte[] signature) {
return Base64.getEncoder().encodeToString(signature);
}
private Optional<URI> buildWebhookServerURI(String webhookServerUrl) {
try {
return Optional.of(new URI(webhookServerUrl));
} catch (URISyntaxException e) {
return Optional.empty();
}
}
private String getDate() {
return headers.get(HEADER_DATE).get(0).split("\"")[0];
}
private String serializePayload(Object payload) {
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}