From 1e546aa4173f6cd1b34ee16cd5c4b76771cd040c Mon Sep 17 00:00:00 2001 From: Yessiest Date: Thu, 1 Feb 2024 21:43:34 +0400 Subject: [PATCH] JWT cookies --- lib/landline.rb | 1 + lib/landline/dsl/methods_path.rb | 9 ++++-- lib/landline/dsl/methods_probe.rb | 2 +- lib/landline/util/cookie.rb | 8 ++--- lib/landline/util/jwt.rb | 54 +++++++++++++++++++++++++++++++ lib/landline/util/parseutils.rb | 20 ++++++------ 6 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 lib/landline/util/jwt.rb diff --git a/lib/landline.rb b/lib/landline.rb index 499e712..4254449 100644 --- a/lib/landline.rb +++ b/lib/landline.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'landline/util/jwt' require_relative 'landline/server' require_relative 'landline/path' require_relative 'landline/probe' diff --git a/lib/landline/dsl/methods_path.rb b/lib/landline/dsl/methods_path.rb index 2e0becd..5b0d0ac 100644 --- a/lib/landline/dsl/methods_path.rb +++ b/lib/landline/dsl/methods_path.rb @@ -69,6 +69,9 @@ module Landline block end + alias before preprocess + alias after postprocess + # Add a filter to the path. # Blocks path access if a filter returns false. # @param block [#call] @@ -81,7 +84,7 @@ module Landline # Include an application as a child of path. # @param filename [String] def plugin(filename) - self.define_singleton_method(:run) do |object| + define_singleton_method(:run) do |object| unless object.is_a? Landline::Node raise ArgumentError, "not a node instance or subclass instance" end @@ -89,9 +92,9 @@ module Landline object end @origin.children.append( - self.instance_eval(File.read(filename), filename) + instance_eval(File.read(filename), filename) ) - self.singleton_class.undef_method :run + singleton_class.undef_method :run end end end diff --git a/lib/landline/dsl/methods_probe.rb b/lib/landline/dsl/methods_probe.rb index e1c9c4c..4c332a3 100644 --- a/lib/landline/dsl/methods_probe.rb +++ b/lib/landline/dsl/methods_probe.rb @@ -132,7 +132,7 @@ module Landline # @note reads request.input - may nullify request.body. # @return [Object] def json - JSON.load(request.input) + JSON.parse(request.input) end # Open a file relative to current filepath diff --git a/lib/landline/util/cookie.rb b/lib/landline/util/cookie.rb index 53333ae..fa50ba1 100644 --- a/lib/landline/util/cookie.rb +++ b/lib/landline/util/cookie.rb @@ -10,7 +10,7 @@ ParserCommon = Landline::Util::ParserCommon if RUBY_ENGINE == 'jruby' # fix for JRuby OpenSSL::HMAC.define_singleton_method(:base64digest) do |*args| - Base64.encode64(OpenSSL::HMAC.digest(*args)).strip + Base64.strict_encode64(OpenSSL::HMAC.digest(*args)).strip end end @@ -29,11 +29,11 @@ module Landline # @option params [String, Date] "expires" # @raise Landline::ParsingError invalid cookie parameters def initialize(key, value, params = {}) - unless key.match? HeaderRegexp::COOKIE_NAME + unless key.match?(/\A#{HeaderRegexp::COOKIE_NAME}\z/o) raise Landline::ParsingError, "invalid cookie key: #{key}" end - unless value.match? HeaderRegexp::COOKIE_VALUE + unless value.match?(/\A#{HeaderRegexp::COOKIE_VALUE}\z/o) raise Landline::ParsingError, "invalid cookie value: #{value}" end @@ -88,7 +88,7 @@ module Landline # @param sep [String] Hash separator # @return [Boolean] whether value is signed and valid def verify(key, algorithm: "sha256", sep: "&") - val, sig = @value.match(/\A(.*)#{sep}([A-Za-z0-9+\/=]+)\Z/).to_a[1..] + val, sig = @value.match(/\A(.*)#{sep}([A-Za-z0-9+\/=]+)\z/).to_a[1..] return false unless val and sig sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val) diff --git a/lib/landline/util/jwt.rb b/lib/landline/util/jwt.rb new file mode 100644 index 0000000..e329b2c --- /dev/null +++ b/lib/landline/util/jwt.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'openssl' +require 'json' +require 'base64' + +if RUBY_ENGINE == 'jruby' # fix for JRuby + OpenSSL::HMAC.define_singleton_method(:base64digest) do |*args| + Base64.strict_encode64(OpenSSL::HMAC.digest(*args)).strip + end +end + +module Landline + module Util + # JSON Web Token construction class + class JWT + # Create a new JWT token wrapper + # @param data [Hash, Array] JSON-formattable data + # @param halgo [String] Name of the hash algorithm to use + def initialize(data, halgo = "SHA256") + @halgo = halgo + @data = data + end + + # Construct a string representation of the current token + # @param key [String] + # @return [String] + def make(key) + jsondata = @data.to_json + [ + { + "hash" => @halgo + }.to_json, + jsondata, + OpenSSL::HMAC.digest(@halgo, key, jsondata) + ].map(&Base64.method(:strict_encode64)).map(&:strip).join "&" + end + + # Construct an object from string + # @param input [String] + # @param key [String] + # @return [JWT, nil] returns nil if verification couldn't complete + def self.from_string(input, key) + halgoj, dataj, sig = input.split("&").map(&Base64.method(:strict_decode64)) + halgo = JSON.parse(halgoj)["hash"] + return nil if OpenSSL::HMAC.digest(halgo, key, dataj) != sig + + new(JSON.parse(dataj), halgo) + end + + attr_accessor :data + end + end +end diff --git a/lib/landline/util/parseutils.rb b/lib/landline/util/parseutils.rb index 9b8d2eb..17d5d73 100644 --- a/lib/landline/util/parseutils.rb +++ b/lib/landline/util/parseutils.rb @@ -8,28 +8,28 @@ module Landline # (not exactly precise) Regular expressions for some RFC definitions module HeaderRegexp # Matches the RFC2616 definiton of token - TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/.freeze + TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/ # Matches the RFC2616 definition of quoted-string - QUOTED = /"[\x0-\x7E&&[^\x1-\x8\xb-\x1f]]*(?