Class: Line::Bot::V2::WebhookParser

Inherits:
Object
  • Object
show all
Defined in:
lib/line/bot/v2/webhook_parser.rb,
sig/line/bot/v2/webhook_parser.rbs

Defined Under Namespace

Classes: InvalidSignatureError

Instance Method Summary collapse

Constructor Details

#initialize(channel_secret:, skip_signature_verification: nil) ⇒ WebhookParser

Initialize webhook parser

Parameters:

  • channel_secret (String)

    The channel secret used for signature verification.

  • skip_signature_verification (() -) (defaults to: nil)

    bool, nil] A callable object with type () -> bool that determines whether to skip webhook signature verification. Signature verification is skipped if and only if this callable is provided and returns true. This can be useful in scenarios such as when you're in the process of updating the channel secret and need to temporarily bypass verification to avoid disruptions.

  • channel_secret: (String)
  • skip_signature_verification: ((^() -> bool), nil) (defaults to: nil)


25
26
27
28
# File 'lib/line/bot/v2/webhook_parser.rb', line 25

def initialize(channel_secret:, skip_signature_verification: nil)
  @channel_secret = channel_secret
  @skip_signature_verification = skip_signature_verification
end

Instance Method Details

#parse(body:, signature:) ⇒ Array<Line::Bot::V2::Webhook::Event>

Parse events from the raw request body and validate the signature.

Examples:

Sinatra usage

def parser
  @parser ||= Line::Bot::V2::WebhookParser.new(
    channel_secret: ENV.fetch("LINE_CHANNEL_SECRET"),
    skip_signature_verification: -> { ENV['SKIP_SIGNATURE_VERIFICATION'] == 'true' }
  )
end

post '/callback' do
  body = request.body.read
  signature = request.env['HTTP_X_LINE_SIGNATURE']

  begin
    events = parser.parse(body: body, signature: signature)
  rescue Line::Bot::V2::WebhookParser::InvalidSignatureError
    halt 400, { 'Content-Type' => 'text/plain' }, 'Bad Request'
  end

  # Handle events...
  events.each do |event|
    case event
    when Line::Bot::V2::Webhook::MessageEvent
      handle_message_event(event)
    ...
  end
  "OK"
end

Parameters:

  • body (String)

    The unmodified request body (exactly as received).

  • signature (String)

    The value of the 'X-LINE-Signature' header.

Returns:

  • (Array<Line::Bot::V2::Webhook::Event>)

    An array of event objects. Recognized events become instances of classes under Line::Bot::V2::Webhook::*Event; Line::Bot::V2::Webhook::Event is returned as fallback only when the event class is not defined in line-bot-sdk library. When you update the SDK, you may not need to handle Line::Bot::V2::Webhook::Event anymore.

Raises:



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/line/bot/v2/webhook_parser.rb', line 71

def parse(body:, signature:)
  should_skip = @skip_signature_verification&.call || false

  unless should_skip == true || verify_signature(body: body, signature: signature)
    raise InvalidSignatureError.new("Invalid signature: #{signature}")
  end

  data = JSON.parse(body.chomp, symbolize_names: true)
  data = Line::Bot::V2::Utils.deep_underscore(data)
  data = Line::Bot::V2::Utils.deep_convert_reserved_words(data)
  data = Line::Bot::V2::Utils.deep_symbolize(data)

  data[:events].map do |event|
    Line::Bot::V2::Webhook::Event.create(**event) # steep:ignore
  end
end

#secure_compare(a, b) ⇒ Boolean

Parameters:

  • (String)
  • (String)

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
109
# File 'lib/line/bot/v2/webhook_parser.rb', line 101

def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  l = a.unpack("C#{a.bytesize}")

  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift } # steep:ignore ArgumentTypeMismatch
  res == 0
end

#variable_secure_compare(a, b) ⇒ Boolean

To avoid timing attacks

Parameters:

  • (String)
  • (String)

Returns:

  • (Boolean)


97
98
99
# File 'lib/line/bot/v2/webhook_parser.rb', line 97

def variable_secure_compare(a, b)
  secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))
end

#verify_signature(body:, signature:) ⇒ Boolean

Parameters:

  • body: (String)
  • signature: (String)

Returns:

  • (Boolean)


88
89
90
91
92
# File 'lib/line/bot/v2/webhook_parser.rb', line 88

def verify_signature(body:, signature:)
  hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @channel_secret, body)
  expected = Base64.strict_encode64(hash)
  variable_secure_compare(signature, expected)
end