Skip to content

Commit

Permalink
Implement webhook plugin
Browse files Browse the repository at this point in the history
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
sudowork committed Apr 29, 2021
1 parent ee05c46 commit c51cfb6
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 0 deletions.
46 changes: 46 additions & 0 deletions de1plus/plugins/webhook/README.md
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.
129 changes: 129 additions & 0 deletions de1plus/plugins/webhook/plugin.tcl
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] \
} ]
}
}
4 changes: 4 additions & 0 deletions de1plus/plugins/webhook/settings.tdb
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

0 comments on commit c51cfb6

Please sign in to comment.