Skip to content

Instantly share code, notes, and snippets.

@tcaddy
Created January 9, 2024 23:50

Revisions

  1. tcaddy created this gist Jan 9, 2024.
    3 changes: 3 additions & 0 deletions Gemfile
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    source "https://rubygems.org"

    gem "functions_framework", "~> 1.4"
    21 changes: 21 additions & 0 deletions Gemfile.lock
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    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
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    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.
    93 changes: 93 additions & 0 deletions app.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,93 @@
    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