Skip to content

Instantly share code, notes, and snippets.

@tcaddy
Created January 9, 2024 23:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tcaddy/d1dd1b9270bcd694dae2de3014a79d30 to your computer and use it in GitHub Desktop.
Save tcaddy/d1dd1b9270bcd694dae2de3014a79d30 to your computer and use it in GitHub Desktop.
getport.io Webhook validation in Ruby

This is a proof-of-concept for validation Webhooks from https://getport.io. This is the source for a GCP Cloud Function for Ruby 3.2.

require 'functions_framework'
require 'base64'
require 'openssl'
FunctionsFramework.http "entrypoint" do |request|
# The request parameter is a Rack::Request object.
# See https://www.rubydoc.info/gems/rack/Rack/Request
WebhookProcessor.new(request: request).call
end
class WebhookProcessor
# See: https://docs.getport.io/create-self-service-experiences/security/
GET_PORT_IP_ADDRESSES = [
'44.221.30.248', '44.193.148.179', '34.197.132.205', '3.251.12.205',
].freeze
HEADERS = {
get_port: {
signature: 'X_PORT_SIGNATURE',
timestamp: 'X_PORT_TIMESTAMP'
}
}.freeze
def call
return four_zero_four unless valid_signature?
process
response
end
private
def initialize(request:)
@request = request
@response = {}
end
def response
# response can be:
# * a string
# * a Ruby Hash (which will be converted to a JSON-encoded string)
# * an instance of `Rack::Response`
# * A Rack response array
@response
end
def four_zero_four
Rack::Response.new('not authorized', 401)
end
def process
puts "TODO: do stuff here to handle webhook"
@response[:msg] = "OK"
end
def request_originated_from_get_port?
(GET_PORT_IP_ADDRESSES & @request.forwarded_for).any?
end
def expected_signature
case
when request_originated_from_get_port? then
@request.get_header("HTTP_#{HEADERS[:get_port][:signature]}").split(',')[1]
else nil
end
end
def computed_signature
case
when request_originated_from_get_port? then
Base64.strict_encode64(
OpenSSL::HMAC.digest(
'sha256',
ENV['GET_PORT_CLIENT_SECRET'],
[
@request.get_header("HTTP_#{HEADERS[:get_port][:timestamp]}"),
@request.body.string
].join('.')
)
)
else nil
end
end
def valid_signature?
case
when request_originated_from_get_port? then
Rack::Utils.secure_compare(computed_signature, expected_signature)
else false
end
end
end
source "https://rubygems.org"
gem "functions_framework", "~> 1.4"
GEM
remote: https://rubygems.org/
specs:
cloud_events (0.7.1)
functions_framework (1.4.1)
cloud_events (>= 0.7.0, < 2.a)
puma (>= 4.3.0, < 7.a)
rack (>= 2.1, < 4.a)
nio4r (2.5.9)
puma (6.4.0)
nio4r (~> 2.0)
rack (3.0.8)
PLATFORMS
ruby
DEPENDENCIES
functions_framework (~> 1.4)
BUNDLED WITH
2.4.10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment