Compare commits

...

5 Commits

Author SHA1 Message Date
Yessiest 1e546aa417 JWT cookies 2024-02-01 21:43:34 +04:00
Yessiest dce0937e2e bumped version 2023-11-11 20:42:48 +04:00
Yessiest eb7e44a537 removed an oopsie 2023-11-11 20:31:07 +04:00
Yessiest 59aa1206ff Added more methods to process various types of post forms 2023-10-25 10:49:58 +04:00
Yessiest 9030b5ef05 Modified guidelines, bumped version in library 2023-10-22 01:44:33 +04:00
12 changed files with 186 additions and 50 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/*.gem /*.gem
/doc /doc
/.yardoc /.yardoc
/examples/uploader/files/*

View File

@ -23,8 +23,3 @@ To keep things beautiful, consider following recommendations:
- Document classes as if the next maintainer after you has you at gunpoint. - Document classes as if the next maintainer after you has you at gunpoint.
Document thoroughly, use YARD tags and **never** skip on public method Document thoroughly, use YARD tags and **never** skip on public method
docs and class docs. As an example, consider Landline::PatternMatching::Glob. docs and class docs. As an example, consider Landline::PatternMatching::Glob.
- Unit tests suck for many reasons. However, if you're writing a class that
does not have any dependents and which is frequently used, consider making
a unit test for it. People that might have to fix things further along
will be very thankful.

View File

@ -8,15 +8,17 @@ layout as closely as possible.
These are core classes of Landline and they are loaded as soon as the library is loaded. These are core classes of Landline and they are loaded as soon as the library is loaded.
- Landline::Path [path.rb] - Landline::Path [path.rb]
- Landline::PathBinding [path.rb] - Landline::PathContext [path.rb]
- Landline::Probe [probe.rb] - Landline::Probe [probe.rb]
- Landline::ProbeBinding [probe.rb] - Landline::ProbeContext [probe.rb]
- Landline::Node (parent of Path and Probe) [node.rb] - Landline::Node (parent of Path and Probe) [node.rb]
- Landline::Server (Rack application interface) [server.rb] - Landline::Server (Rack application interface) [server.rb]
- Landline::ServerBinding [server.rb] - Landline::ServerContext [server.rb]
- Landline::Request (Rack request wrapper) [request.rb] - Landline::Request (Rack request wrapper) [request.rb]
- Landline::Response (Rack response wrapper) [response.rb] - Landline::Response (Rack response wrapper) [response.rb]
- Landline::Pattern [pattern\_matching.rb] - Landline::Pattern [pattern\_matching.rb]
- Landline::TemplateContext [tempalte.rb]
- Landline::Template (template engine interface) [template.rb]
## Patterns ## Patterns
@ -29,31 +31,56 @@ These are classes that Landline::Pattern can interface with to create Patterns.
These are module mixins that add common methods to DSL bindings. These are module mixins that add common methods to DSL bindings.
- Landline::DSL::PathConstructors [dsl/path\_constructors.rb] - Landline::DSL::PathConstructors [dsl/constructors\_path.rb]
- Landline::DSL::ProbeConstructures [dsl/constructors\_probe.rb]
- Landline::DSL::CommonMethods [dsl/methods\_common.rb]
- Landline::DSL::PathMethods [dsl/methods\_path.rb]
- Landline::DSL::ProbeMethods [dsl/methods\_probe.rb]
- Landline::DSL::TemplateMethods [dsl/methods\_template.rb]
## Utilities ## Utilities
These are self-contained classes and methods that add extra functionality to Landline. These are self-contained classes and methods that add extra functionality to Landline.
- Landline::Util::Lookup [util/lookup.rb] - Landline::Util::Lookup [util/lookup.rb]
- Landline::PatternMatching [pattern\_matching/util.rb]
- Landline::Cookie (class) [util/cookie.rb]
- Landline::Error (class) [util/errors.rb]
- Landline::ParsingError (class) [util/errors.rb]
- Landline::Util (html/http utilities) [util/html.rb]
- Landline::MIME (MIME extension to type association) [util/mime.rb]
- Landline::Util::ParserSorting (functions for sorting form/query hashes) [util/parsesorting.rb]
- Landline::Util::Query (query class) [util/query.rb]
- Landline::Util::FormPart (formparser struct) [util/multipart.rb]
- Landline::Util::MultipartParser (multipart form parser) [util/multipart.rb]
- Landline::Util::HeaderRegexp (helper regexps for headers) [util/parseutils.rb]
- Landline::Util (parser methods) [util/parseutils.rb]
## Probe subclasses ## Probe subclasses
These are reactive request handlers with their own semantics, if needed. These are reactive request handlers with their own semantics, if needed.
- Landline::Handler [probe/handler.rb] - Landline::Handlers::Handler [probe/handler.rb]
- Landline::GETHandler [probe/http\_method.rb] - Landline::Handlers::GETHandler [probe/http\_method.rb]
- Landline::POSTHandler [probe/http\_method.rb] - Landline::Handlers::POSTHandler [probe/http\_method.rb]
- Landline::HEADHandler [probe/http\_method.rb] - Landline::Handlers::HEADHandler [probe/http\_method.rb]
- Landline::PUTHandler [probe/http\_method.rb] - Landline::Handlers::PUTHandler [probe/http\_method.rb]
- Landline::DELETEHandler [probe/http\_method.rb] - Landline::Handlers::DELETEHandler [probe/http\_method.rb]
- Landline::CONNECTHandler [probe/http\_method.rb] - Landline::Handlers::CONNECTHandler [probe/http\_method.rb]
- Landline::OPTIONSHandler [probe/http\_method.rb] - Landline::Handlers::OPTIONSHandler [probe/http\_method.rb]
- Landline::TRACEHandler [probe/http\_method.rb] - Landline::Handlers::TRACEHandler [probe/http\_method.rb]
- Landline::PATCHHandler [probe/http\_method.rb] - Landline::Handlers::PATCHHandler [probe/http\_method.rb]
- Landline::Handlers::Serve
## Path subclasses ## Path subclasses
These are navigation handlers with their own semantics. These are navigation handlers with their own semantics.
(currently none) (currently none)
## Template engine interfaces
These are uniform interfaces for various templating engines.
- Landline::Templates::ERB [template/erb.rb]
- Landline::Templates::Erubi [template/erubi.rb]

Binary file not shown.

View File

@ -2,7 +2,7 @@
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "landline" spec.name = "landline"
spec.version = "0.10.0" spec.version = "0.11.0"
spec.summary = "Elegant HTTP DSL" spec.summary = "Elegant HTTP DSL"
spec.description = <<~DESC spec.description = <<~DESC
Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun. Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun.

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'landline/util/jwt'
require_relative 'landline/server' require_relative 'landline/server'
require_relative 'landline/path' require_relative 'landline/path'
require_relative 'landline/probe' require_relative 'landline/probe'
@ -10,10 +11,10 @@ require_relative 'landline/template'
# Landline is a hideously simple ruby web framework # Landline is a hideously simple ruby web framework
module Landline module Landline
# Landline version # Landline version
VERSION = '0.9 "Moonsong" (beta/rewrite)' VERSION = '0.11 "Decades of science" (beta)'
# Landline branding and version # Landline branding and version
VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n" VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n".freeze
# Landline copyright # Landline copyright
COPYRIGHT = "Copyright 2023 Yessiest" COPYRIGHT = "Copyright 2023 Yessiest"

View File

@ -42,13 +42,13 @@ module Landline
# Set root path (appends matched part of the path). # Set root path (appends matched part of the path).
# @param path [String] # @param path [String]
def root(path) def root(path)
@origin.root = path @origin.root = File.expand_path(path)
end end
# Set root path (without appending matched part). # Set root path (without appending matched part).
# @param path [String] # @param path [String]
def remap(path) def remap(path)
@origin.remap = path @origin.remap = File.expand_path(path)
end end
# Add a preprocessor to the path. # Add a preprocessor to the path.
@ -69,6 +69,9 @@ module Landline
block block
end end
alias before preprocess
alias after postprocess
# Add a filter to the path. # Add a filter to the path.
# Blocks path access if a filter returns false. # Blocks path access if a filter returns false.
# @param block [#call] # @param block [#call]
@ -81,7 +84,7 @@ module Landline
# Include an application as a child of path. # Include an application as a child of path.
# @param filename [String] # @param filename [String]
def plugin(filename) def plugin(filename)
self.define_singleton_method(:run) do |object| define_singleton_method(:run) do |object|
unless object.is_a? Landline::Node unless object.is_a? Landline::Node
raise ArgumentError, "not a node instance or subclass instance" raise ArgumentError, "not a node instance or subclass instance"
end end
@ -89,9 +92,9 @@ module Landline
object object
end end
@origin.children.append( @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 end
end end

View File

@ -4,6 +4,7 @@ require_relative '../response'
require_relative '../util/multipart' require_relative '../util/multipart'
require_relative '../util/parseutils' require_relative '../util/parseutils'
require_relative '../util/html' require_relative '../util/html'
require 'json'
module Landline module Landline
module DSL module DSL
@ -85,18 +86,12 @@ module Landline
# Checks if current request has multipart/form-data associated with it # Checks if current request has multipart/form-data associated with it
# @return [Boolean] # @return [Boolean]
def form? def form?
value, opts = Landline::Util::ParserCommon.parse_value( value, opts = _verify_content_type('multipart/form-data')
request.headers["content-type"] !!(value && opts && opts['boundary'])
)
if value == "multipart/form-data" and
opts["boundary"]
true
else
false
end
end end
# Returns formdata # Returns formdata
# @note reads request.input - may nullify request.body.
# @return [Hash{String=>(String,Landline::Util::FormPart)}] # @return [Hash{String=>(String,Landline::Util::FormPart)}]
def form def form
_, opts = Landline::Util::ParserCommon.parse_value( _, opts = Landline::Util::ParserCommon.parse_value(
@ -107,6 +102,39 @@ module Landline
).to_h ).to_h
end end
# Checks if current request has urlencoded query string
# @return [Boolean]
def query?
!!_verify_content_type("application/x-www-form-urlencode")
end
# Returns parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data
def query
Landline::Util::Query.new(request.body).parse
end
# Returns shallow parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data
def query_shallow
Landline::Util::Query.new(request.body).parse_shallow
end
# Check if body is a JSON object
# @return [Boolean]
def json?
!!_verify_content_type('application/json')
end
# Return parse JSON object
# @note reads request.input - may nullify request.body.
# @return [Object]
def json
JSON.parse(request.input)
end
# Open a file relative to current filepath # Open a file relative to current filepath
# @see File.open # @see File.open
def file(path, mode = "r", *all, &block) def file(path, mode = "r", *all, &block)
@ -124,6 +152,22 @@ module Landline
def unescape_html(text) def unescape_html(text)
Landline::Util.unescape_html(text) Landline::Util.unescape_html(text)
end end
private
def _verify_content_type(type)
return false unless request.headers['content-type']
value, opts = Landline::Util::ParserCommon.parse_value(
request.headers["content-type"]
)
if value == type and
request.input
[value, opts]
else
false
end
end
end end
end end
end end

View File

@ -41,12 +41,14 @@ module Landline
end end
# Returns request body (if POST data exists) # Returns request body (if POST data exists)
# @note reads data from rack.input, which is not rewindable. .body data is memoized.
# @return [nil, String] # @return [nil, String]
def body def body
@body ||= @rack.input&.read @body ||= @rack.input&.read
end end
# Returns raw Rack input object # Returns raw Rack input object
# @note Rack IO is not always rewindable - if it is read once, the data is gone (i.e. request.body will return nothing).
# @return [IO] (May not entirely be compatible with IO, see Rack/SPEC.rdoc) # @return [IO] (May not entirely be compatible with IO, see Rack/SPEC.rdoc)
def input def input
@rack.input @rack.input

View File

@ -4,9 +4,16 @@ require_relative 'parseutils'
require_relative 'errors' require_relative 'errors'
require 'date' require 'date'
require 'openssl' require 'openssl'
require 'base64'
HeaderRegexp = Landline::Util::HeaderRegexp HeaderRegexp = Landline::Util::HeaderRegexp
ParserCommon = Landline::Util::ParserCommon ParserCommon = Landline::Util::ParserCommon
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 Landline
# Utility class for handling cookies # Utility class for handling cookies
class Cookie class Cookie
@ -22,11 +29,11 @@ module Landline
# @option params [String, Date] "expires" # @option params [String, Date] "expires"
# @raise Landline::ParsingError invalid cookie parameters # @raise Landline::ParsingError invalid cookie parameters
def initialize(key, value, params = {}) 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}" raise Landline::ParsingError, "invalid cookie key: #{key}"
end end
unless value.match? HeaderRegexp::COOKIE_VALUE unless value.match?(/\A#{HeaderRegexp::COOKIE_VALUE}\z/o)
raise Landline::ParsingError, "invalid cookie value: #{value}" raise Landline::ParsingError, "invalid cookie value: #{value}"
end end
@ -81,7 +88,7 @@ module Landline
# @param sep [String] Hash separator # @param sep [String] Hash separator
# @return [Boolean] whether value is signed and valid # @return [Boolean] whether value is signed and valid
def verify(key, algorithm: "sha256", sep: "&") 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 return false unless val and sig
sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val) sig == ::OpenSSL::HMAC.base64digest(algorithm, key, val)

54
lib/landline/util/jwt.rb Normal file
View File

@ -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

View File

@ -8,28 +8,28 @@ module Landline
# (not exactly precise) Regular expressions for some RFC definitions # (not exactly precise) Regular expressions for some RFC definitions
module HeaderRegexp module HeaderRegexp
# Matches the RFC2616 definiton of token # Matches the RFC2616 definiton of token
TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/.freeze TOKEN = /[!-~&&[^()<>@,;:\\"\/\[\]?={}\t]]+/
# Matches the RFC2616 definition of quoted-string # Matches the RFC2616 definition of quoted-string
QUOTED = /"[\x0-\x7E&&[^\x1-\x8\xb-\x1f]]*(?<!\\)"/.freeze QUOTED = /"[\x0-\x7E&&[^\x1-\x8\xb-\x1f]]*(?<!\\)"/
# Matches any CHAR except CTLs # Matches any CHAR except CTLs
PRINTCHAR = /[\x2-\x7E]/.freeze PRINTCHAR = /[\x2-\x7E]/
# Matches 1 or more CHARs excluding CTLs # Matches 1 or more CHARs excluding CTLs
PRINTABLE = /#{PRINTCHAR}+/o.freeze PRINTABLE = /#{PRINTCHAR}+/o
# Matches the RFC6265 definition of a cookie-octet # Matches the RFC6265 definition of a cookie-octet
COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/.freeze COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/
COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o.freeze COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o
COOKIE_NAME = TOKEN COOKIE_NAME = TOKEN
# Matches the RFC6265 definition of cookie-pair. # Matches the RFC6265 definition of cookie-pair.
# Captures name (1) and value (2). # Captures name (1) and value (2).
COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\Z/o.freeze COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\z/o
# Matches a very abstract definition of a quoted header paramter. # Matches a very abstract definition of a quoted header paramter.
# Captures name (1) and value (2). # Captures name (1) and value (2).
PARAM_QUOTED = /\A(#{TOKEN})=?(#{QUOTED}|#{PRINTCHAR}*)\Z/o.freeze PARAM_QUOTED = /\A(#{TOKEN})=?(#{QUOTED}|#{PRINTCHAR}*)\z/o
# Matches a very abstract definition of a header parameter. # Matches a very abstract definition of a header parameter.
# Captures name (1) and value (2). # Captures name (1) and value (2).
PARAM = /\A(#{TOKEN})=?(#{PRINTCHAR}*)\Z/o.freeze PARAM = /\A(#{TOKEN})=?(#{PRINTCHAR}*)\z/o
# Specifically matches cookie parameters # Specifically matches cookie parameters
COOKIE_PARAM = /\A(#{TOKEN})=?(#{QUOTED}|#{COOKIE_OCTET})\Z/o.freeze COOKIE_PARAM = /\A(#{TOKEN})=?(#{QUOTED}|#{COOKIE_OCTET})\z/o
end end
# Module for all things related to parsing HTTP and related syntax. # Module for all things related to parsing HTTP and related syntax.
@ -48,7 +48,9 @@ module Landline
# @param regexp [Regexp,nil] override param matching regexp # @param regexp [Regexp,nil] override param matching regexp
# @return [Array(String, Hash)] # @return [Array(String, Hash)]
def self.parse_value(input, sep: ";", unquote: false, regexp: nil) def self.parse_value(input, sep: ";", unquote: false, regexp: nil)
parts = input.split(sep).map { |x| URI.decode_uri_component(x).strip } parts = input.split(sep).map do |x|
URI.decode_www_form_component(x).strip
end
base = parts.shift base = parts.shift
opts = parts.map do |raw| opts = parts.map do |raw|
key, value = raw.match(if regexp key, value = raw.match(if regexp