Compare commits

...

3 Commits

7 changed files with 1359 additions and 7 deletions

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require 'securerandom'
require 'landline'
require 'landline/util/cookie'
require 'irb'
KEY = SecureRandom.base64(64).freeze
app = Landline::Server.new do
get "/set-cookie" do
cookie "test", (rand * 500).floor.to_s, { hmac: KEY }
header "content-type", "text/plain"
"Cookie set! Visit /get-cookie to view it"
end
get "/get-cookie" do
header "content-type", "text/plain"
if request.cookies.dig('test', 0)&.verify(KEY)
"Cookie is valid and generated by server"
else
"Cookie either doesn't exist or is forged"
end
end
end
run app

1
examples/cookies-hmac/lib Symbolic link
View File

@ -0,0 +1 @@
../../lib

View File

@ -0,0 +1,3 @@
shows basic usage of unsigned cookies
please note that this does not sign cookies.

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative "../probe" require_relative "../probe"
require_relative "../util/mime"
module Landline module Landline
module Handlers module Handlers
@ -18,12 +19,18 @@ module Landline
# Method callback on successful request navigation. # Method callback on successful request navigation.
# Tries to serve files matched by handler # Tries to serve files matched by handler
# @param request [Landline::Request] # @param request [Landline::Request]
# @return [Boolean] true if file was found # @return [Boolean, Array] true if file was found
def process(request) def process(request)
path = File.expand_path(request.filepath) path = File.expand_path(request.filepath)
return unless path.start_with? @properties["path"] return unless path.start_with? @properties["path"]
File.open(path.delete_suffix("/")) filepath = path.delete_suffix("/")
[200,
{
"content-type" => Landline::MIME.get_mime_type(filepath)
},
File.open(filepath)]
rescue StandardError rescue StandardError
false false
end end

View File

@ -26,7 +26,7 @@ module Landline
def finalize def finalize
@cookies.each do |_, cookie_array| @cookies.each do |_, cookie_array|
cookie_array.each do |cookie| cookie_array.each do |cookie|
add_header("set-cookie", cookie.to_s) add_header("set-cookie", cookie.finalize)
end end
end end
[@status, @headers, @body] [@status, @headers, @body]

View File

@ -3,6 +3,7 @@
require_relative 'parseutils' require_relative 'parseutils'
require_relative 'errors' require_relative 'errors'
require 'date' require 'date'
require 'openssl'
HeaderRegexp = Landline::Util::HeaderRegexp HeaderRegexp = Landline::Util::HeaderRegexp
ParserCommon = Landline::Util::ParserCommon ParserCommon = Landline::Util::ParserCommon
@ -29,15 +30,22 @@ module Landline
raise Landline::ParsingError, "invalid cookie value: #{value}" raise Landline::ParsingError, "invalid cookie value: #{value}"
end end
# Make param keys strings
params.transform_keys!(&:to_s)
# Primary cookie parameters # Primary cookie parameters
@key = key @key = key
@value = value @value = value
setup_params(params) setup_params(params)
# Cookie signing parameters
setup_hmac(params)
end end
# Convert cookie to "Set-Cookie: " string representation. # Convert cookie to "Set-Cookie: " string representation.
# @return [String] # @return [String]
def to_s def finalize
sign(@hmac, algorithm: @algorithm, sep: @sep) if @hmac
ParserCommon.make_value( ParserCommon.make_value(
"#{key.to_s.strip}=#{value.to_s.strip}", "#{key.to_s.strip}=#{value.to_s.strip}",
{ {
@ -54,10 +62,31 @@ module Landline
# Convert cookie to "Cookie: " string representation (no params) # Convert cookie to "Cookie: " string representation (no params)
# @return [String] # @return [String]
def to_short def finalize_short
sign(@hmac, algorithm: @algorithm, sep: @sep) if @hmac
"#{key.to_s.strip}=#{value.to_s.strip}" "#{key.to_s.strip}=#{value.to_s.strip}"
end end
# Sign the cookie value with HMAC
# @param key [String] HMAC signing key
# @param algorithm [String] Hash algorithm to use
# @param sep [String] Hash separator
def sign(key, algorithm: "sha256", sep: "&")
@value += sep + ::OpenSSL::HMAC.base64digest(algorithm, key, @value)
end
# Verify HMAC signature
# @param key [String] HMAC signing key
# @param algorithm [String] Hash algorithm
# @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..]
return false unless val and sig
sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val)
end
attr_accessor :key, :value attr_accessor :key, :value
attr_reader :domain, :path, :expires, :maxage, :samesite, :secure, :httponly attr_reader :domain, :path, :expires, :maxage, :samesite, :secure, :httponly
@ -66,7 +95,7 @@ module Landline
# @return [Cookie] # @return [Cookie]
def self.from_setcookie_string(data) def self.from_setcookie_string(data)
kvpair, params = parse_value(data, regexp: HeaderRegexp::COOKIE_PARAM) kvpair, params = parse_value(data, regexp: HeaderRegexp::COOKIE_PARAM)
key, value = kvpair.split("=").map(&:strip) key, value = kvpair.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
Cookie.new(key, value, params) Cookie.new(key, value, params)
end end
@ -75,8 +104,11 @@ module Landline
# @return [Hash{String => Cookie}] # @return [Hash{String => Cookie}]
def self.from_cookie_string(data) def self.from_cookie_string(data)
hash = {} hash = {}
return hash if data.nil?
data.split(";").map do |cookiestr| data.split(";").map do |cookiestr|
cookie = Cookie.new(*cookiestr.split("=").map(&:strip)) key, value = cookiestr.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
cookie = Cookie.new(key, value)
if hash[cookie.key] if hash[cookie.key]
hash[cookie.key].append(cookie) hash[cookie.key].append(cookie)
else else
@ -88,6 +120,12 @@ module Landline
private private
def setup_hmac(params)
@hmac = params['hmac']
@algorithm = (params['algorithm'] or "sha256")
@sep = (params['sep'] or "&")
end
def setup_params(params) def setup_params(params)
# Extended cookie params # Extended cookie params
params.transform_keys!(&:downcase) params.transform_keys!(&:downcase)

1276
lib/landline/util/mime.rb Normal file

File diff suppressed because it is too large Load Diff