Skip to content

added webhook verification to mux_ruby + helper-func infrastructure #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*.rb]
indent_size = 2
indent_style = space
insert_final_newline = true
13 changes: 10 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ on:
jobs:
build:
name: Integration Test
strategy:
fail-fast: false
matrix:
ruby: ['2.7', head]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Install Ruby 2.6
uses: actions/setup-ruby@v1
- name: Install Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.x
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Install Bundler
run: gem install bundler
- name: Install Ruby Dependencies
run: bundle install --jobs 4 --retry 3
- name: Run unit test
run: rake spec
- name: Run Integration Tests
env:
MUX_TOKEN_ID: ${{ secrets.MUX_TOKEN_ID }}
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,28 @@ on:
jobs:
build:
name: Release to RubyGems
strategy:
fail-fast: false
matrix:
ruby: ['2.7', head]
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Install Ruby 2.6
uses: actions/setup-ruby@v1
- name: Install Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.x
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Update system rubygems
run: gem update --system
#
- name: Install Bundler
run: gem install bundler
- name: Install Ruby Dependencies
run: bundle install --jobs 4 --retry 3
- name: Run unit test
run: rake spec
- name: Run Integration Tests
env:
MUX_TOKEN_ID: ${{ secrets.MUX_TOKEN_ID }}
Expand Down
8 changes: 8 additions & 0 deletions .openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@

git_push.sh
.travis.yml

lib/helpers.rb
lib/helpers/*.rb

spec/api_client_spec.rb
spec/configuration_spec.rb
spec/api/*.rb
spec/models/*.rb
2 changes: 0 additions & 2 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,4 @@ lib/mux_ruby/models/video_view_event.rb
lib/mux_ruby/models/video_view_response.rb
lib/mux_ruby/version.rb
mux_ruby.gemspec
spec/api_client_spec.rb
spec/configuration_spec.rb
spec/spec_helper.rb
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
PATH
remote: .
specs:
mux_ruby (3.0.0)
mux_ruby (3.3.1)
jwt (~> 2.3)
securecompare (= 1.0.0)
typhoeus (~> 1.0, >= 1.0.1)

GEM
Expand All @@ -15,6 +17,7 @@ GEM
ffi (>= 1.15.0)
ffi (1.15.4)
jaro_winkler (1.5.4)
jwt (2.3.0)
method_source (1.0.0)
parallel (1.20.1)
parser (3.0.2.0)
Expand Down Expand Up @@ -50,6 +53,7 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.6)
ruby-progressbar (1.11.0)
securecompare (1.0.0)
solid_assert (1.0.0)
typhoeus (1.4.0)
ethon (>= 0.9.0)
Expand Down
4 changes: 2 additions & 2 deletions docs/AssetNonStandardInputReasons.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
| ---- | ---- | ----------- | ----- |
| **video_codec** | **String** | The video codec used on the input file. For example, the input file encoded with &#x60;hevc&#x60; video codec is non-standard and the value of this parameter is &#x60;hevc&#x60;. | [optional] |
| **audio_codec** | **String** | The audio codec used on the input file. Non-AAC audio codecs are non-standard. | [optional] |
| **video_gop_size** | **String** | The video key frame Interval (also called as Group of Picture or GOP) of the input file is &#x60;high&#x60;. This parameter is present when the gop is greater than 10 seconds. | [optional] |
| **video_frame_rate** | **String** | The video frame rate of the input file. Video with average frames per second (fps) less than 10 or greater than 120 is non-standard. A &#x60;-1&#x60; frame rate value indicates Mux could not determine the frame rate of the video track. | [optional] |
| **video_gop_size** | **String** | The video key frame Interval (also called as Group of Picture or GOP) of the input file is &#x60;high&#x60;. This parameter is present when the gop is greater than 20 seconds. | [optional] |
| **video_frame_rate** | **String** | The video frame rate of the input file. Video with average frames per second (fps) less than 5 or greater than 120 is non-standard. A &#x60;-1&#x60; frame rate value indicates Mux could not determine the frame rate of the video track. | [optional] |
| **video_resolution** | **String** | The video resolution of the input file. Video resolution higher than 2048 pixels on any one dimension (height or width) is considered non-standard, The resolution value is presented as &#x60;width&#x60; x &#x60;height&#x60; in pixels. | [optional] |
| **video_bitrate** | **String** | The video bitrate of the input file is &#x60;high&#x60;. This parameter is present when the average bitrate of any key frame interval (also known as Group of Pictures or GOP) is higher than what&#39;s considered standard which typically is 16 Mbps. | [optional] |
| **pixel_aspect_ratio** | **String** | The video pixel aspect ratio of the input file. | [optional] |
Expand Down
4 changes: 3 additions & 1 deletion docs/CreateLiveStreamRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| **latency_mode** | **String** | Latency is the time from when the streamer transmits a frame of video to when you see it in the player. Set this as an alternative to setting low latency or reduced latency flags. The Low Latency value is a beta feature. Note: Reconnect windows are incompatible with Reduced Latency and Low Latency and will always be set to zero (0) seconds. Read more here: https://mux.com/blog/introducing-low-latency-live-streaming/ | [optional] |
| **test** | **Boolean** | Marks the live stream as a test live stream when the value is set to true. A test live stream can help evaluate the Mux Video APIs without incurring any cost. There is no limit on number of test live streams created. Test live streams are watermarked with the Mux logo and limited to 5 minutes. The test live stream is disabled after the stream is active for 5 mins and the recorded asset also deleted after 24 hours. | [optional] |
| **simulcast_targets** | [**Array&lt;CreateSimulcastTargetRequest&gt;**](CreateSimulcastTargetRequest.md) | | [optional] |
| **max_continuous_duration** | **Integer** | The time in seconds a live stream may be continuously active before being disconnected. Defaults to 12 hours. | [optional][default to 43200] |

## Example

Expand All @@ -32,7 +33,8 @@ instance = MuxRuby::CreateLiveStreamRequest.new(
low_latency: null,
latency_mode: null,
test: null,
simulcast_targets: null
simulcast_targets: null,
max_continuous_duration: null
)
```

4 changes: 3 additions & 1 deletion docs/LiveStream.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| **simulcast_targets** | [**Array&lt;SimulcastTarget&gt;**](SimulcastTarget.md) | Each Simulcast Target contains configuration details to broadcast (or \&quot;restream\&quot;) a live stream to a third-party streaming service. [See the Stream live to 3rd party platforms guide](https://docs.mux.com/guides/video/stream-live-to-3rd-party-platforms). | [optional] |
| **latency_mode** | **String** | Latency is the time from when the streamer transmits a frame of video to when you see it in the player. Set this as an alternative to setting low latency or reduced latency flags. The Low Latency value is a beta feature. Note: Reconnect windows are incompatible with Reduced Latency and Low Latency and will always be set to zero (0) seconds. Read more here: https://mux.com/blog/introducing-low-latency-live-streaming/ | [optional] |
| **test** | **Boolean** | True means this live stream is a test live stream. Test live streams can be used to help evaluate the Mux Video APIs for free. There is no limit on the number of test live streams, but they are watermarked with the Mux logo, and limited to 5 minutes. The test live stream is disabled after the stream is active for 5 mins and the recorded asset also deleted after 24 hours. | [optional] |
| **max_continuous_duration** | **Integer** | The time in seconds a live stream may be continuously active before being disconnected. Defaults to 12 hours. | [optional][default to 43200] |

## Example

Expand All @@ -44,7 +45,8 @@ instance = MuxRuby::LiveStream.new(
low_latency: null,
simulcast_targets: null,
latency_mode: null,
test: null
test: null,
max_continuous_duration: null
)
```

4 changes: 3 additions & 1 deletion docs/UpdateLiveStreamRequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| **passthrough** | **String** | Arbitrary user-supplied metadata set for the live stream. Max 255 characters. In order to clear this value, the field should be included with an empty-string value. | [optional] |
| **latency_mode** | **String** | Latency is the time from when the streamer transmits a frame of video to when you see it in the player. Set this as an alternative to setting low latency or reduced latency flags. The Low Latency value is a beta feature. Note: Reconnect windows are incompatible with Reduced Latency and Low Latency and will always be set to zero (0) seconds. Read more here: https://mux.com/blog/introducing-low-latency-live-streaming/ | [optional] |
| **reconnect_window** | **Float** | When live streaming software disconnects from Mux, either intentionally or due to a drop in the network, the Reconnect Window is the time in seconds that Mux should wait for the streaming software to reconnect before considering the live stream finished and completing the recorded asset. | [optional] |
| **max_continuous_duration** | **Integer** | The time in seconds a live stream may be continuously active before being disconnected. Defaults to 12 hours. | [optional][default to 43200] |

## Example

Expand All @@ -16,7 +17,8 @@ require 'mux_ruby'
instance = MuxRuby::UpdateLiveStreamRequest.new(
passthrough: null,
latency_mode: null,
reconnect_window: null
reconnect_window: null,
max_continuous_duration: null
)
```

1 change: 1 addition & 0 deletions gen/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ clean-products:

build: ensure clean-products
${OAS_CLI} generate -g "${GENERATOR_TYPE}" -c "${CONFIG_PATH}" -t "${TEMPLATE_DIR}" -o "${OUTPUT_DIR}" -i "${OAS_SPEC_PATH}"
cp -R ${OUTPUT_DIR}/lib-manual/* ${OUTPUT_DIR}/lib/mux_ruby

config-help: ensure
${OAS_CLI} config-help -g "${GENERATOR_TYPE}"
Expand Down
3 changes: 3 additions & 0 deletions gen/templates/gem.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ require '{{importPath}}'
{{/apis}}
{{/apiInfo}}

# Custom imports
require 'mux_ruby/helpers'

module {{moduleName}}
class << self
# Customize default settings for the SDK using block.
Expand Down
2 changes: 2 additions & 0 deletions gen/templates/gemspec.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Gem::Specification.new do |s|
{{^isFaraday}}
s.add_runtime_dependency 'typhoeus', '~> 1.0', '>= 1.0.1'
{{/isFaraday}}
s.add_runtime_dependency 'securecompare', '1.0.0'
s.add_runtime_dependency 'jwt', '~> 2.3'

s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'

Expand Down
1 change: 1 addition & 0 deletions lib-manual/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'mux_ruby/helpers/webhook_verifier'
76 changes: 76 additions & 0 deletions lib-manual/helpers/webhook_verifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require 'openssl'
require 'securecompare'

module MuxRuby
module Helpers
class WebhookVerifier
DEFAULT_TOLERANCE = 300
HEADER_SCHEMES = [:v1].freeze

# Initializer.
#
# @param [String] secret the webhook secret from your Mux control panel
# @param [Integer] tolerance the signature timing tolerance, in seconds (generally leave as is)
# @param [Array<Symbol>] header_schemes the list of accepted header schemes for this verifier
def initialize(secret: nil, tolerance: DEFAULT_TOLERANCE, header_schemes: [:v1])
raise "secret '#{secret.inspect}' must be a String" unless secret.is_a?(String)
raise "tolerance '#{tolerance.inspect}' must be a positive number." \
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we use tolerance anywhere else, typically it's expiration. Why the change here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tolerance in the node SDK.

unless tolerance.is_a?(Integer) && tolerance > 0
raise "header schemes '#{header_schemes.inspect}' must all be in HEADER_SCHEMES: '#{HEADER_SCHEMES.inspect}'" \
unless header_schemes.all? { |h| HEADER_SCHEMES.include?(h) }

@secret = secret.dup
@tolerance = tolerance
@header_schemes = header_schemes.map(&:to_s).sort.uniq.reverse
end

# Initializer.
#
# @param [String] request_body the raw, unmodified body of the request
# @param [String] header the Mux-Signature header
# @param [Time] current_timestamp (for test purposes) the current time expected for this webhook (defaults to `Time.utc`)
# @return [Boolean] true if webhook is verified; false otherwise
def verify(request_body:, header:, current_timestamp: Time.now.getutc)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember this from Ruby land circa 2013, but is arg:, just specifying a nullish default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It specifies a named argument rather than a positional argument. I think they're clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmcc this must have come after your Ruby time (it was right near the end of my Ruby time) -- keyword args, which are a fantastic language feature:

https://thoughtbot.com/blog/ruby-2-keyword-arguments

In this case calling verify without request_body: and header: args would cause a runtime error.

In the past we would sometimes use an options hash and then have conditionals inside method to check for things that were passed into the options hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this just replaces the options hash.

@dylanjha , I didn't know you were a Ruby guy. Oh, do I have code for you to review...!

header_parts = self.kv_from_header(header)

timestamp = header_parts['t'].to_i
scheme_used = @header_schemes.detect { |scheme| !header_parts[scheme].nil? }
mux_signature = header_parts[scheme_used]

if timestamp.nil? || mux_signature.nil?
false
else
case scheme_used
when 'v1'
expected_signature = self.compute_v1_signature("#{timestamp}.#{request_body}")
if SecureCompare.compare(expected_signature, mux_signature)
delta = current_timestamp.to_i - timestamp

if (delta <= @tolerance)
true
else
false
end
else
false
end
else
warn "Unhandled *but recognized* Mux signature format '#{scheme_used}'. Please contact Mux."
false
end
end
end

private
def kv_from_header(header)
Hash[header.strip.gsub(/^Mux-Signature:/, "").strip.split(",").map { |tup| tup.split("=") }]
end

def compute_v1_signature(payload)
OpenSSL::HMAC.hexdigest("SHA256", @secret, payload)
end
end
end
end
3 changes: 3 additions & 0 deletions lib/mux_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
require 'mux_ruby/api/url_signing_keys_api'
require 'mux_ruby/api/video_views_api'

# Custom imports
require 'mux_ruby/helpers'

module MuxRuby
class << self
# Customize default settings for the SDK using block.
Expand Down
1 change: 1 addition & 0 deletions lib/mux_ruby/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'mux_ruby/helpers/webhook_verifier'
76 changes: 76 additions & 0 deletions lib/mux_ruby/helpers/webhook_verifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

require 'openssl'
require 'securecompare'

module MuxRuby
module Helpers
class WebhookVerifier
DEFAULT_TOLERANCE = 300
HEADER_SCHEMES = [:v1].freeze

# Initializer.
#
# @param [String] secret the webhook secret from your Mux control panel
# @param [Integer] tolerance the signature timing tolerance, in seconds (generally leave as is)
# @param [Array<Symbol>] header_schemes the list of accepted header schemes for this verifier
def initialize(secret: nil, tolerance: DEFAULT_TOLERANCE, header_schemes: [:v1])
raise "secret '#{secret.inspect}' must be a String" unless secret.is_a?(String)
raise "tolerance '#{tolerance.inspect}' must be a positive number." \
unless tolerance.is_a?(Integer) && tolerance > 0
raise "header schemes '#{header_schemes.inspect}' must all be in HEADER_SCHEMES: '#{HEADER_SCHEMES.inspect}'" \
unless header_schemes.all? { |h| HEADER_SCHEMES.include?(h) }

@secret = secret.dup
@tolerance = tolerance
@header_schemes = header_schemes.map(&:to_s).sort.uniq.reverse
end

# Initializer.
#
# @param [String] request_body the raw, unmodified body of the request
# @param [String] header the Mux-Signature header
# @param [Time] current_timestamp (for test purposes) the current time expected for this webhook (defaults to `Time.utc`)
# @return [Boolean] true if webhook is verified; false otherwise
def verify(request_body:, header:, current_timestamp: Time.now.getutc)
header_parts = self.kv_from_header(header)

timestamp = header_parts['t'].to_i
scheme_used = @header_schemes.detect { |scheme| !header_parts[scheme].nil? }
mux_signature = header_parts[scheme_used]

if timestamp.nil? || mux_signature.nil?
false
else
case scheme_used
when 'v1'
expected_signature = self.compute_v1_signature("#{timestamp}.#{request_body}")
if SecureCompare.compare(expected_signature, mux_signature)
delta = current_timestamp.to_i - timestamp

if (delta <= @tolerance)
true
else
false
end
else
false
end
else
warn "Unhandled *but recognized* Mux signature format '#{scheme_used}'. Please contact Mux."
false
end
end
end

private
def kv_from_header(header)
Hash[header.strip.gsub(/^Mux-Signature:/, "").strip.split(",").map { |tup| tup.split("=") }]
end

def compute_v1_signature(payload)
OpenSSL::HMAC.hexdigest("SHA256", @secret, payload)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/mux_ruby/models/asset_non_standard_input_reasons.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class AssetNonStandardInputReasons
# The audio codec used on the input file. Non-AAC audio codecs are non-standard.
attr_accessor :audio_codec

# The video key frame Interval (also called as Group of Picture or GOP) of the input file is `high`. This parameter is present when the gop is greater than 10 seconds.
# The video key frame Interval (also called as Group of Picture or GOP) of the input file is `high`. This parameter is present when the gop is greater than 20 seconds.
attr_accessor :video_gop_size

# The video frame rate of the input file. Video with average frames per second (fps) less than 10 or greater than 120 is non-standard. A `-1` frame rate value indicates Mux could not determine the frame rate of the video track.
# The video frame rate of the input file. Video with average frames per second (fps) less than 5 or greater than 120 is non-standard. A `-1` frame rate value indicates Mux could not determine the frame rate of the video track.
attr_accessor :video_frame_rate

# The video resolution of the input file. Video resolution higher than 2048 pixels on any one dimension (height or width) is considered non-standard, The resolution value is presented as `width` x `height` in pixels.
Expand Down
Loading