-
Notifications
You must be signed in to change notification settings - Fork 51
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provides the following settings: webhook_domain webhook_endpoint webhook_secret_key webhook_trigger_after Sends an HTTP webhook to: ``` POST https://$webhook_domain$webhook_endpoint Authorization: $clock!$hmac_signature $body ``` The HMAC signature is used so your server can verify the authenticity of a message. The signature is created from: `HMAC($webhook_secret_key, "$path\n$clock\n$body")`
- Loading branch information
Showing
3 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# "Webhook" DE1app plugin | ||
|
||
This plugin provides functionality to trigger a webhook after a shot has completed. | ||
|
||
Much of the code is directly inspired by the `visualizer_upload` plugin. | ||
|
||
## Why? | ||
|
||
If you are a programmer, you may have some automations or other things you may want to trigger as a result of pulling a shot of espresso. Having a way to receive a webhook, or push notification in a sense, makes it much easier to trigger your automation. | ||
|
||
## Settings | ||
|
||
The plugin provides the following settings to be configured: | ||
|
||
- `webhook_domain` (default: `example.com`) - The domain/origin for the website serving the webhook. This should _not_ include the protocol or any part of the path. | ||
- `webhook_endpoint` (default: `/decent/webhook`) - The resource path where the webhook should POST to. | ||
- `webhook_secret_key` (default: `SecretKeyForSigning`) - This is the key used for generating the HMAC signature, which is passed as part of the `Authorization` header. | ||
- `webhook_trigger_after` (default: `10`) - Number of seconds to wait after flow completes before triggering the webhook. | ||
|
||
## Behavior | ||
|
||
- After a shot completes (`after_flow_complete`) and after the built-in `after_flow_complete_delay`, wait an additional `webhook_trigger_after` seconds. | ||
- Send a `POST` request to the specified webhook URL | ||
- Connect to the `webhook_domain` over TLS. | ||
- Construct the body as a file attachment of the history file | ||
- Calculate the HMAC signature | ||
- Send the `POST` request with the appropriate `Authorization` header to `https://$webhook_domain$webhook_endpoint`. | ||
|
||
## HMAC Verification | ||
|
||
As a best practice with webhooks, you should be verifying the authenticity of the message being sent. We use HMAC to accomplish this to verify both the sender's identity and the message's validity. The plugin will automatically calculate an HMAC signature and send it in the `Authorization` header. The header will look something like below: | ||
|
||
``` | ||
Authorization: 1617092331!f728a8092b6882749efa61a6609414d14c6f25286bdb1fd76e0dda4bab0f0cdf | ||
^ clock ^ HMAC signature | ||
``` | ||
|
||
Where `1617092331` represents the clock at the time of the shot, and `f728a8092b6882749efa61a6609414d14c6f25286bdb1fd76e0dda4bab0f0cdf` is the calculated HMAC signature. Note that they are delimited by the character `!`. | ||
|
||
The signature calculation for verifying on your server-side should look like the following: | ||
|
||
``` | ||
HMAC_SHA256($webhook_secret_key, "$webhook_endpoint\n$clock\n$body") | ||
``` | ||
|
||
It can also be good to check against replay attacks by verifying that the clock time is not too old. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package require http | ||
package require sha256 | ||
|
||
set plugin_name "webhook" | ||
|
||
namespace eval ::plugins::${plugin_name} { | ||
|
||
variable author "Kevin Gao" | ||
variable contact "@sudowork" | ||
variable version 1.0 | ||
variable description "Sends shot data as a webhook to a specified URL" | ||
variable name "Webhook" | ||
|
||
proc preload {} { | ||
return [build_settings_ui] | ||
} | ||
|
||
proc build_settings_ui {} { | ||
# Create settings if non-existant | ||
if {[array size ::plugins::webhook::settings] == 0} { | ||
array set ::plugins::webhook::settings { | ||
webhook_domain "example.com" | ||
webhook_endpoint "/decent/webhook" | ||
webhook_secret_key "SecretKeyForSigning" | ||
webhook_trigger_after 10 | ||
} | ||
plugins save_settings webhook | ||
} | ||
|
||
# Unique name per page | ||
set page_name "plugin_webhook_page_default" | ||
|
||
# Background image and "Done" button | ||
add_de1_page "$page_name" "settings_message.png" "default" | ||
add_de1_text $page_name 1280 1310 -text [translate "Done"] -font Helv_10_bold -fill "#fAfBff" -anchor "center" | ||
add_de1_button $page_name {say [translate {Done}] $::settings(sound_button_in); page_to_show_when_off extensions} 980 1210 1580 1410 "" | ||
|
||
# Headline | ||
add_de1_text $page_name 1280 300 -text [translate "Webhook"] -font Helv_20_bold -width 1200 -fill "#444444" -anchor "center" -justify "center" | ||
|
||
# Render Settings | ||
build_setting_text_input $page_name [translate "Webhook Domain"] {::plugins::webhook::settings(webhook_domain)} 480 | ||
build_setting_text_input $page_name [translate "Webhook Endpoint"] {::plugins::webhook::settings(webhook_endpoint)} 660 | ||
build_setting_text_input $page_name [translate "Webhook Secret Key"] {::plugins::webhook::settings(webhook_secret_key)} 840 | ||
build_setting_text_input $page_name [translate "Trigger After (seconds)"] {::plugins::webhook::settings(webhook_trigger_after)} 1020 | ||
|
||
return $page_name | ||
} | ||
|
||
proc build_setting_text_input {page_name label var y} { | ||
set x 280 | ||
add_de1_text $page_name $x $y -text $label -font Helv_8 -width 300 -fill "#444444" -anchor "nw" -justify "center" | ||
add_de1_widget $page_name entry $x [expr $y + 60] { | ||
bind $widget <Return> { say [translate {save}] $::settings(sound_button_in); borg toast [translate "Saved"]; save_plugin_settings webhook; hide_android_keyboard} | ||
bind $widget <Leave> hide_android_keyboard | ||
} -width [expr {int(38 * $::globals(entry_length_multiplier))}] -font Helv_8 -borderwidth 1 -bg #fbfaff -foreground #4e85f4 -textvariable "$var" -relief flat -highlightthickness 1 -highlightcolor #000000 | ||
} | ||
|
||
proc post_shot_data {} { | ||
variable settings | ||
|
||
msg "triggering webhook" | ||
borg toast [translate "Triggering Webhook"] | ||
|
||
http::register https 443 [list ::tls::socket -servername $settings(webhook_domain)] | ||
|
||
# Craft HTTP Body | ||
set espresso_data [format_espresso_for_history] | ||
set clock [clock seconds] | ||
set boundary "--------$clock" | ||
set type "multipart/form-data, charset=utf-8, boundary=$boundary" | ||
set content [encoding convertto utf-8 $espresso_data] | ||
set contentHeader "Content-Disposition: form-data; name=\"file\"; filename=\"file.shot\"\r\nContent-Type: application/octet-stream\r\n" | ||
set body "--$boundary\r\n$contentHeader\r\n$content\r\n--$boundary--\r\n" | ||
|
||
# Build HMAC signature | ||
# HMAC Signature = SHA256(secret, endpoint_path + "\n" + clock + "\n" + body) | ||
set message_for_hmac "$settings(webhook_endpoint)\n$clock\n$body" | ||
set signature [sha2::hmac $settings(webhook_secret_key) $message_for_hmac] | ||
set headerl [list \ | ||
Authorization "$clock!$signature" \ | ||
Content-Type "multipart/form-data; boundary=$boundary"] | ||
|
||
if {[catch { | ||
# HTTPS only for now | ||
set url "https://$settings(webhook_domain)$settings(webhook_endpoint)" | ||
set token [http::geturl $url -headers $headerl -method POST -type $type -query $body -timeout 30000] | ||
set status [http::status $token] | ||
set answer [http::data $token] | ||
set returncode [http::ncode $token] | ||
set returnfullcode [http::code $token] | ||
msg $token | ||
msg "status: $status" | ||
# msg "answer: $answer" | ||
} err] != 0} { | ||
msg "Could not upload shot to webhook! $err" | ||
borg toast [translate "Webhook failed!"] | ||
catch { http::cleanup $token } | ||
return | ||
} | ||
|
||
http::cleanup $token | ||
if {$returncode != 200} { | ||
msg "Webhook failed: $returnfullcode" | ||
borg toast "Webhook failed!" | ||
return | ||
} | ||
|
||
msg "Webhook succeeded!" | ||
borg toast "Webhook succeeded!" | ||
} | ||
|
||
proc async_dispatch {old new} { | ||
after [expr 1000 * $::plugins::webhook::settings(webhook_trigger_after)] ::plugins::webhook::post_shot_data | ||
} | ||
|
||
proc msg { msg } { | ||
::msg [namespace current] $msg | ||
} | ||
|
||
proc main {} { | ||
::de1::event::listener::after_flow_complete_add \ | ||
[lambda {event_dict} { | ||
::plugins::webhook::async_dispatch \ | ||
[dict get $event_dict previous_state] \ | ||
[dict get $event_dict this_state] \ | ||
} ] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
webhook_domain example.com | ||
webhook_endpoint /decent/webhook | ||
webhook_secret_key SecretKeyForSigning | ||
webhook_trigger_after 10 |