Browse Source

(NOT FOR RELEASE) biblically accurate jwt tokens; minor extra features for requests; redirections, jumps, and request looping; rack application class; crosscalls; partial hijacking; session object; proper context description for dsl methods (DO NOT SHIP, VULNERABILITY FOUND)

master
Yessiest 2 weeks ago
parent
commit
e251d2def6
  1. 53
      examples/longpolling.ru
  2. 43
      examples/partial_hijacking.ru
  3. 73
      examples/rack_app.ru
  4. 67
      examples/session.ru
  5. 2
      landline.gemspec
  6. 1
      lib/landline.rb
  7. 50
      lib/landline/app.rb
  8. 21
      lib/landline/dsl/constructors_path.rb
  9. 2
      lib/landline/dsl/constructors_probe.rb
  10. 2
      lib/landline/dsl/methods_common.rb
  11. 17
      lib/landline/dsl/methods_path.rb
  12. 78
      lib/landline/dsl/methods_probe.rb
  13. 1
      lib/landline/dsl/methods_template.rb
  14. 97
      lib/landline/extensions/session.rb
  15. 14
      lib/landline/path.rb
  16. 1
      lib/landline/probe.rb
  17. 25
      lib/landline/probe/crosscall_handler.rb
  18. 61
      lib/landline/request.rb
  19. 2
      lib/landline/response.rb
  20. 61
      lib/landline/server.rb
  21. 52
      lib/landline/util/jwt.rb
  22. 1
      lib/landline/util/mime.rb
  23. 2
      lib/landline/util/query.rb

53
examples/longpolling.ru

@ -0,0 +1,53 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require_relative '../lib/landline'
class Propogator
def initialize
@queue = []
end
# Append a client to the queue
def append(client)
@queue.append(client)
end
# Push data to all clients, releasing all of them at once
def push(data)
@queue.each do |client|
client << data
client.flush
client.close
end
@queue = []
end
end
# @!parse include ::Landline::DSL::PathConstructors
# @!parse include ::Landline::DSL::PathMethods
longpoll_queue = Propogator.new
app = Landline::Server.new do
post "/push_event" do
longpoll_queue.push(request.body)
next request.body
end
get "/await_event" do
next "No long polling for you :(" unless request.hijack?
partial_hijack do |stream|
longpoll_queue.append(stream)
end
next ''
end
get "/ping" do
"pong"
end
end
run app

43
examples/partial_hijacking.ru

@ -0,0 +1,43 @@
# frozen_string_literal: true
$LOAD_PATH.unshift("#{File.dirname(__FILE__)}/lib")
require_relative '../lib/landline'
# @!parse include ::Landline::DSL::PathConstructors
# @!parse include ::Landline::DSL::PathMethods
app = Landline::Server.new do
postprocess do |request, response|
puts response.class
end
get "/hijack" do
# @!parse include ::Landline::DSL::ProbeMethods
if request.hijack?
partial_hijack do |stream|
sec = (rand * 20).floor
sleep(sec)
stream << <<~DOC
<!DOCTYPE html>
<HTML>
<HEAD>
<TITLE>You have been delayed, bozo!</TITLE>
</HEAD>
<BODY>
<H1>Get delayed, dumbass!</H1>
<P>Imagine getting delayed for like, what, #{sec} seconds? LMAO</P>
</BODY>
</HTML>
DOC
stream.flush
stream.close
end
next ''
else
header "content-type", "text/plain"
next 'No partial hijacking for you :('
end
end
end
run app

73
examples/rack_app.ru

@ -0,0 +1,73 @@
# frozen_string_literal: true
require 'landline'
# Example rack middleware
class TimerMiddleware
def initialize(app)
@app = app
end
def call(*data)
puts("Request accepted")
before = Time.now
output = @app.call(*data)
puts("Time elapsed: #{(Time.now - before) * 1000}ms")
output
end
end
# Example Landline application as rack middleware
class HelloServer < Landline::App
setup do
get "/test2" do
"Hello world from #{self}!"
end
handle do |status, backtrace: nil|
page = ([Landline::Util::HTTP_STATUS[status]] +
(backtrace || [""])).join("\n")
[
{
"content-length": page.bytesize,
"content-type": "text/plain",
"x-cascade": true
},
page
]
end
end
end
# Example Landline app as rack application
class CrossCallServer < Landline::App
setup do
get "/inner_test" do
"Hello world, through crosscall!"
end
end
end
# Example Landline app as rack application
class Server < Landline::App
use TimerMiddleware
use HelloServer
setup do
crosscall_server = CrossCallServer.new
get "/test" do
"Hello from #{self}!"
end
# Cross-callable application included as a subpath
link "/outer", crosscall_server
# Cross calling an application in a probe context
get "/crosscall" do
request.path = "/inner_test"
call(crosscall_server)
end
end
end
run Server.new

67
examples/session.ru

@ -0,0 +1,67 @@
# frozen_string_literal: true
require 'landline/extensions/session'
require 'landline'
Landline::Session.hmac_secret = "Your secure signing secret here"
app = Landline::Server.new do
get "/make_cookie" do
session["random_number"] = Random.random_number(100)
text = <<~HTML
<!DOCTYPE html>
<html>
<head>
<title>
Session test #{request.to_s}
</title>
</head>
<body>
<h1>Your random number is #RAND#!</h1>
<hr>
<p>Go check it at <a href="/check_cookie">this link!</a></p>
</body>
</html>
HTML
sleep(20)
text.gsub("#RAND#", request.to_s)
end
get "/check_cookie" do
if session["random_number"]
<<~HTML
<!DOCTYPE html>
<html>
<head>
<title>
Session test
</title>
</head>
<body>
<h1>Your random number is...</h1>
<hr>
<p>#{session['random_number']}! Enjoy your random magic number!</p>
</body>
</html>
HTML
else
<<~HTML
<!DOCTYPE html>
<html>
<head>
<title>
Session test
</title>
</head>
<body>
<h1>Uh oh!</h1>
<hr>
<p>Go get your magic number at <a href="/make_cookie">this link!</a></p>
</body>
</html>
HTML
end
end
end
run app

2
landline.gemspec

@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
It is usable for many menial tasks, and as long as it continues to be fun, it will keep growing.
DESC
spec.authors = ["Yessiest"]
spec.license = "AGPL-3.0"
spec.license = "AGPL-3.0-or-later"
spec.email = "yessiest@text.512mb.org"
spec.homepage = "https://adastra7.net/git/Yessiest/landline"
spec.files = Dir["lib/**/*"]

1
lib/landline.rb

@ -7,6 +7,7 @@ require_relative 'landline/probe'
require_relative 'landline/request'
require_relative 'landline/response'
require_relative 'landline/template'
require_relative 'landline/app'
# Landline is a hideously simple ruby web framework
module Landline

50
lib/landline/app.rb

@ -0,0 +1,50 @@
# frozen_string_literal: true
module Landline
# Rack application interface
class App
# TODO: fix this mess somehow (probably impossible)
# @!parse include Landline::DSL::PathMethods
# @!parse include Landline::DSL::PathConstructors
# @!parse include Landline::DSL::ProbeConstructors
# @!parse include Landline::DSL::ProbeMethods
# @!parse include Landline::DSL::CommonMethods
class << self
# Duplicate used middleware for the subclassed app
def inherited(subclass)
super(subclass)
subclass.middleware = @middleware.dup
end
# Include a middleware in application
# @param middleware [Class]
def use(middleware)
@middleware ||= []
@middleware.append(middleware)
end
# Setup block
# @param block [#call]
def setup(&block)
@setup_block = block
end
attr_accessor :middleware, :setup_block
end
def initialize(*args, **opts)
@app = ::Landline::Server.new(*args, **opts, &self.class.setup_block)
self.class.middleware&.reverse_each do |cls|
@app = cls.new(@app)
end
end
# Rack ingress point.
# @param env [Hash]
# @return [Array(Integer,Hash,Array)]
def call(env)
@app.call(env)
end
end
end

21
lib/landline/dsl/constructors_path.rb

@ -5,6 +5,7 @@ module Landline
module DSL
# Path (and subclasses) DSL constructors
module PathConstructors
# (in Landline::Path context)
# Append a Node child object to the list of children
def register(obj)
unless obj.is_a? Landline::Node
@ -14,11 +15,13 @@ module Landline
@origin.children.append(obj)
end
# (in Landline::Path context)
# Create a new {Landline::Path} object
def path(path, **args, &setup)
register(Landline::Path.new(path, parent: @origin, **args, &setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::Probe} object
def probe(path, **args, &_setup)
register(Landline::Handlers::Probe.new(path,
@ -26,6 +29,7 @@ module Landline
**args))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::GETHandler} object
def get(path, **args, &setup)
register(Landline::Handlers::GET.new(path,
@ -34,6 +38,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# create a new {Landline::Handlers::POSTHandler} object
def post(path, **args, &setup)
register(Landline::Handlers::POST.new(path,
@ -42,6 +47,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::PUTHandler} object
def put(path, **args, &setup)
register(Landline::Handlers::PUT.new(path,
@ -50,6 +56,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::HEADHandler} object
def head(path, **args, &setup)
register(Landline::Handlers::HEAD.new(path,
@ -58,6 +65,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::DELETEHandler} object
def delete(path, **args, &setup)
register(Landline::Handlers::DELETE.new(path,
@ -66,6 +74,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::CONNECTHandler} object
def connect(path, **args, &setup)
register(Landline::Handlers::CONNECT.new(path,
@ -74,6 +83,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::TRACEHandler} object
def trace(path, **args, &setup)
register(Landline::Handlers::TRACE.new(path,
@ -82,6 +92,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::PATCHHandler} object
def patch(path, **args, &setup)
register(Landline::Handlers::PATCH.new(path,
@ -90,6 +101,7 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::OPTIONSHandler} object
def options(path, **args, &setup)
register(Landline::Handlers::OPTIONS.new(path,
@ -98,10 +110,19 @@ module Landline
&setup))
end
# (in Landline::Path context)
# Create a new {Landline::Handlers::GETHandler} that serves static files
def serve(path)
register(Landline::Handlers::Serve.new(path, parent: @origin))
end
# (in Landline::Path context)
# Create a new application crosscall link (acts like #call in probe context and strips its path from request)
def link(path, application)
register(Landline::Handlers::Link.new(path,
application,
parent: @origin))
end
end
end
end

2
lib/landline/dsl/constructors_probe.rb

@ -4,6 +4,7 @@ module Landline
module DSL
# Probe (and subclasses) DSL construct
module ProbeConstructors
# (in Landline::Probe context)
# Create a new erb template
# @see Landline::Template#new
def erb(input, vars = {})
@ -13,6 +14,7 @@ module Landline
filename: caller_locations[0].path)
end
# (in Landline::Probe context)
# Create a new erb template using Erubi engine
# @see Landline::Template#new
# @param freeze [Boolean] whether to use frozen string literal

2
lib/landline/dsl/methods_common.rb

@ -4,6 +4,7 @@ module Landline
module DSL
# Methods shared by probes, preprocessors and filters.
module CommonMethods
# (in Landline::Probe context)
# Stop execution and generate a boilerplate response with the given code
# @param errorcode [Integer]
# @param backtrace [Array(String), nil]
@ -18,6 +19,7 @@ module Landline
)
end
# (in Landline::Probe context)
# Bounce request to the next handler
# @raise [UncaughtThrowError] throws :break to get out of the callback
def bounce

17
lib/landline/dsl/methods_path.rb

@ -5,11 +5,19 @@ module Landline
module DSL
# Common path methods
module PathMethods
# (in Landline::Path context)
# Bounce request if no handler found instead of issuing 404
def bounce
@origin.bounce = true
end
# (in Landline::Path context)
# Unset bounce
def nobounce
@origin.bounce = false
end
# (in Landline::Path context)
# Create a status code handler on path.
# Recursively applies to all paths unless overridden.
# @param code [Integer, nil] Specify a status code to handle
@ -18,6 +26,7 @@ module Landline
@origin.properties["handle.#{code || 'default'}"] = block
end
# (in Landline::Path context)
# Insert a pass-through pipeline into request processing
# (i.e. for error handling purposes).
# Passed block should yield request (and return yielded data back).
@ -26,6 +35,7 @@ module Landline
@origin.pipeline = block
end
# (in Landline::Path context)
# Set path index
# @param index [Array,String]
def index(index)
@ -39,18 +49,21 @@ module Landline
end
end
# (in Landline::Path context)
# Set root path (appends matched part of the path).
# @param path [String]
def root(path)
@origin.root = File.expand_path(path)
end
# (in Landline::Path context)
# Set root path (without appending matched part).
# @param path [String]
def remap(path)
@origin.remap = File.expand_path(path)
end
# (in Landline::Path context)
# Add a preprocessor to the path.
# Does not modify path execution.
# @param block [#call]
@ -60,6 +73,7 @@ module Landline
block
end
# (in Landline::Path context)
# Add a postprocessor to the path.
# @param block [#call]
# @yieldparam request [Landline::Request]
@ -72,6 +86,7 @@ module Landline
alias before preprocess
alias after postprocess
# (in Landline::Path context)
# Add a filter to the path.
# Blocks path access if a filter returns false.
# @param block [#call]
@ -81,7 +96,9 @@ module Landline
block
end
# (in Landline::Path context)
# Include an application as a child of path.
# @deprecated this method is being deprecated due to strong dependency on the framework
# @param filename [String]
def plugin(filename)
define_singleton_method(:run) do |object|

78
lib/landline/dsl/methods_probe.rb

@ -10,12 +10,14 @@ module Landline
module DSL
# Common methods for Probe objects
module ProbeMethods
# (in Landline::Probe context)
# Get the current request
# @return [Landline::Request]
def request
@origin.request
end
# (in Landline::Probe context)
# Set response status (generate response if one doesn't exist yet)
# @param status [Integer] http status code
def status(status)
@ -23,8 +25,59 @@ module Landline
@origin.response.status = status
end
# (in Landline::Probe context)
# Add a finalizer callable to the response
# @param callable [#call]
def defer(&callable)
rack = @origin.request.rack
if rack.respond_to?(:response_finished)
rack.response_finished.append(callable)
end
# puma for some reason isn't compatible with the 3.0.0 spec on this
rack.after_reply.append(callable) if rack.respond_to?(:after_reply)
end
# (in Landline::Probe context)
# Do serverside request redirection
# @note this essentially reprocesses the whole request - be mindful of processing time!
# @param path [String]
def jump(path)
@origin.request.path = path
throw(:break, [307, { "x-internal-jump": true }, []])
end
# (in Landline::Probe context)
# Do clientside request redirection via 302 code
# @param path [String]
def redirect(path)
throw(:break, [302, { "location": path }, []])
end
# (in Landline::Probe context)
# Do clientside request redirection via 307 code
# @param path [String]
def redirect_with_method(path)
throw(:break, [307, { "location": path }, []])
end
alias code status
# (in Landline::Probe context)
# Set a partial hijack callback
# @param block [#call] Callable block
def partial_hijack(&block)
@origin.response ||= Landline::Response.new
@origin.response.add_header("rack.hijack", block)
end
# (in Landline::Probe context)
# Fully hijack IO
# @return [IO]
def hijack
@origin.request.hijack.call
end
# (in Landline::Probe context)
# Set response header (generate response if one doesn't exist yet)
# @param key [String] header name
# @param value [String] header value
@ -40,10 +93,11 @@ module Landline
end
@origin.response = (@origin.response or Landline::Response.new)
key = key.downcase
key = key.downcase.to_s
@origin.response.add_header(key, value)
end
# (in Landline::Probe context)
# Delete a header value from the headers hash
# If no value is provided, deletes all key entries
# @param key [String] header name
@ -61,9 +115,10 @@ module Landline
raise ArgumentError, "value key has invalid characters"
end
@origin.response.delete_header(key, value)
@origin.response.delete_header(key.to_s, value)
end
# (in Landline::Probe context)
# Set response cookie
# @see Landline::Cookie.new
def cookie(*params, **options)
@ -73,6 +128,7 @@ module Landline
)
end
# (in Landline::Probe context)
# Delete a cookie
# If no value is provided, deletes all cookies with the same key
# @param key [String] cookie key
@ -83,6 +139,7 @@ module Landline
@origin.response.delete_cookie(key, value)
end
# (in Landline::Probe context)
# Checks if current request has multipart/form-data associated with it
# @return [Boolean]
def form?
@ -90,6 +147,7 @@ module Landline
!!(value && opts && opts['boundary'])
end
# (in Landline::Probe context)
# Returns formdata
# @note reads request.input - may nullify request.body.
# @return [Hash{String=>(String,Landline::Util::FormPart)}]
@ -102,12 +160,14 @@ module Landline
).to_h
end
# (in Landline::Probe context)
# Checks if current request has urlencoded query string
# @return [Boolean]
def query?
!!_verify_content_type("application/x-www-form-urlencode")
end
# (in Landline::Probe context)
# Returns parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data
@ -115,6 +175,7 @@ module Landline
Landline::Util::Query.new(request.body).parse
end
# (in Landline::Probe context)
# Returns shallow parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data
@ -122,12 +183,14 @@ module Landline
Landline::Util::Query.new(request.body).parse_shallow
end
# (in Landline::Probe context)
# Check if body is a JSON object
# @return [Boolean]
def json?
!!_verify_content_type('application/json')
end
# (in Landline::Probe context)
# Return parse JSON object
# @note reads request.input - may nullify request.body.
# @return [Object]
@ -135,24 +198,35 @@ module Landline
JSON.parse(request.input)
end
# (in Landline::Probe context)
# Open a file relative to current filepath
# @see File.open
def file(path, mode = "r", *all, &block)
File.open("#{request.filepath}/#{path}", mode, *all, &block)
end
# (in Landline::Probe context)
# Escape HTML entities
# @see Landline::Util.escape_html
def escape_html(text)
Landline::Util.escape_html(text)
end
# (in Landline::Probe context)
# Unescape HTML entities
# @see Landline::Util.escape_html
def unescape_html(text)
Landline::Util.unescape_html(text)
end
# (in Landline::Path context)
# Pass the requested environment to a different application
# @param application [#call] Rack application
# @return [Array(Integer, Hash{String => Object}, Object)] response
def call(application)
application.call(@origin.request.env)
end
private
def _verify_content_type(type)

1
lib/landline/dsl/methods_template.rb

@ -7,6 +7,7 @@ module Landline
module DSL
# Common methods for template contexts
module TemplateMethods
# (in Landline::Template context)
# Import a template part
# @param filepath [String, File] path to the file (or the file itself)
# @return [String] compiled template

97
lib/landline/extensions/session.rb

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'securerandom'
module Landline
# Module for controlling session signing secrets
module Session
# Set hmac secret
# @param secret [String]
def self.hmac_secret=(secret)
@hmac_secret = secret
end
# Get hmac secret
def self.hmac_secret
unless @hmac_secret or ENV['HMAC_SECRET']
warn <<~MSG
warn: hmac secret not supplied, using randomized one
warn: provide hmac secret with $HMAC_SECRET or Landline::Session.hmac_secret
MSG
end
@hmac_secret ||= ENV.fetch('HMAC_SECRET', SecureRandom.base64(80))
end
# Class for representing session storage
class Session
def initialize(cookie, cookies_callback)
@data = if cookie
Landline::Util::JWT.from_string(
cookie,
Landline::Session.hmac_secret
)
else
Landline::Util::JWT.new({})
end
@valid = !@data.nil?
@cookies_callback = cookies_callback
end
# Retrieve data from session storage
# @param key [String, Symbol] serializable key
def [](key)
raise StandardError, "session not valid" unless @valid
unless key.is_a? String or key.is_a? Symbol
raise StandardError, "key not serializable"
end
@data.data[key]
end
# Set data to session storage
# @param key [String, Symbol] serializable key
# @param value [Object] serializable data
def []=(key, value)
raise StandardError, "session not valid" unless @valid
unless key.is_a? String or key.is_a? Symbol
raise StandardError, "key not serializable"
end
@data.data[key] = value
@cookies_callback.call(@data.make(Landline::Session.hmac_secret))
end
attr_reader :valid
end
end
end
module Landline
module DSL
module ProbeMethods
# TODO: If probe execution contexts are somehow shared between threads
# this could result in a session leakage through race condition.
# (in Landline::Probe context)
# Return session storage hash
# @return [Landline::Session::Session]
def session
return @session if @session
@session = Landline::Session::Session.new(
request.cookies.dig('session', 0)&.value,
proc do |value|
delete_cookie("session", value)
cookie("session", value)
end
)
request.postprocessors.append(proc do
@session = nil
end)
@session
end
end
end
end

14
lib/landline/path.rb

@ -50,6 +50,7 @@ module Landline
# Contexts setup
context = self.class::Context.new(self)
context.instance_exec(&setup)
# TODO: This isn't fine
@proccontext = self.class::ProcContext.new(self)
end
@ -132,18 +133,25 @@ module Landline
enqueue_postprocessors(request)
@children.each do |x|
value = x.go(request)
return value if value
return exit_stack(request, value) if value
end
value = index(request)
return value if value
return exit_stack(request, value) if value
@bounce ? false : _die(404)
@bounce ? exit_stack(request) : _die(404)
rescue StandardError => e
_die(500, backtrace: [e.to_s] + e.backtrace)
ensure
@request = nil
end
# Run enqueued postprocessors on navigation failure
# @param request [Landline::Request]
def exit_stack(request, response = nil)
request.run_postprocessors(response)
false
end
# Try to perform indexing on the path if possible
# @param request [Landline::Request]
# @return [Boolean] true if indexing succeeded

1
lib/landline/probe.rb

@ -21,6 +21,7 @@ module Landline
autoload :TRACE, "landline/probe/http_method"
autoload :PATCH, "landline/probe/http_method"
autoload :Serve, "landline/probe/serve_handler"
autoload :Link, "landline/probe/crosscall_handler"
end
# Context that provides execution context for Probes.

25
lib/landline/probe/crosscall_handler.rb

@ -0,0 +1,25 @@
# frozen_string_literal: true
require_relative "../probe"
module Landline
module Handlers
# Probe that sends files from a location
class Link < Landline::Probe
# @param path [Object]
# @param parent [Landline::Node]
def initialize(path, application, parent:)
@application = application
super(path, parent: parent, filepath: true)
end
# Method callback on successful request navigation.
# Sends request over to another rack app, stripping the part of the path that was not navigated
# @param request [Landline::Request]
# @return [Array(Integer, Host{String => Object}, Object)]
def process(request)
throw :finish, @application.call(request.env)
end
end
end
end

61
lib/landline/request.rb

@ -21,10 +21,10 @@ module Landline
@param = {}
@splat = []
# Traversal route. Public and writable.
@path = URI.decode_www_form_component(env["PATH_INFO"].dup)
@path = URI.decode_www_form_component(env["PATH_INFO"])
# File serving path. Public and writable.
@filepath = "/"
# Encapsulates all rack variables. Should not be public.
# Encapsulates all rack variables. Is no longer private, but usually should not be used directly
@rack = init_rack_vars(env)
# Internal navigation states. Private.
@states = []
@ -35,9 +35,10 @@ module Landline
# Run postprocessors
# @param response [Landline::Response]
def run_postprocessors(response)
@postprocessors.each do |postproc|
@postprocessors.reverse_each do |postproc|
postproc.call(self, response)
end
@postprocessors = []
end
# Returns request body (if POST data exists)
@ -64,10 +65,33 @@ module Landline
@path, @param, @splat, @filepath = @states.pop
end
# Checks if response stream can be partially hijacked
def hijack?
@_original_env['rack.hijack?']
end
# Returns full hijack callback
def hijack
@_original_env['rack.hijack']
end
# Reconstructs rack env after modification
def env
path = @path
@_original_env.merge(reconstruct_headers)
.merge({
'PATH_INFO' => path,
'REQUEST_PATH' => path,
'QUERY_STRING' => query.query,
'REQUEST_URI' => "#{path}?#{query.query}"
})
.merge(reconstruct_cookie)
end
attr_reader :request_method, :script_name, :path_info, :server_name,
:server_port, :server_protocol, :headers, :param, :splat,
:postprocessors, :query, :cookies
attr_accessor :path, :filepath
:postprocessors, :cookies, :rack
attr_accessor :path, :filepath, :query
private
@ -119,7 +143,7 @@ module Landline
.freeze
end
# Iniitalize headers hash
# Initialize headers hash
# @param env [Hash]
# @return Hash
def init_headers(env)
@ -133,5 +157,30 @@ module Landline
x.downcase.gsub("_", "-") if x.is_a? String
end.freeze
end
# Reconstruct headers
def reconstruct_headers
@headers.filter_map do |k, v|
next unless v
if !['content-type', 'content-length',
'remote-addr'].include?(k) && (k.is_a? String)
k = "http_#{k}"
end
k = k.upcase.gsub("-", "_")
[k, v]
end.to_h
end
# Reconstruct cookie string
def reconstruct_cookie
return {} if @cookies.empty?
{
"HTTP_COOKIE" => @cookies.map do |_, v|
v.finalize_short
end.join(";")
}
end
end
end

2
lib/landline/response.rb

@ -96,7 +96,7 @@ module Landline
end
end
attr_accessor :status, :headers, :body
attr_accessor :status, :headers, :body, :cookies
# Ensure response correctness
# @param obj [String, Array, Landline::Response]

61
lib/landline/server.rb

@ -11,39 +11,64 @@ module Landline
# A specialized path that can be used directly as a Rack application.
class Server < Landline::Path
Context = ServerContext
# @param parent [Landline::Node, nil] Parent object to inherit properties to
# @param setup [#call] Setup block
def initialize(parent: nil, **args, &setup)
super("", parent: nil, **args, &setup)
def initialize(passthrough = nil, parent: nil, **opts, &setup)
super("", parent: nil, **opts, &setup)
return if parent
@passthrough = passthrough
setup_properties(parent: nil, **opts)
end
# Rack ingress point.
# @param env [Hash]
# @return [Array(Integer,Hash,Array)]
def call(env)
request = Landline::Request.new(env)
response = handle_jumps(request)
request.run_postprocessors(response)
resp = response.finalize
if resp[1][:"x-cascade"] and resp[0] == 404 and @passthrough
@passthrough.call(request.env)
else
resp
end
end
private
# Catch internal jumps
def handle_jumps(request)
response = Response.convert(catch(:finish) do
go(request)
end)
while response and
response.status == 307 and
response.headers.include? :"x-internal-jump"
response = Response.convert(catch(:finish) do
go(request)
end)
end
response
end
# Inititalization block for property setup
def setup_properties(*_args, **_opts)
{
"index" => [],
"handle.default" => proc do |code, backtrace: nil|
page = Landline::Util.default_error_page(code, backtrace)
headers = {
"content-length": page.bytesize,
"content-type": "text/html"
"content-type": "text/html",
"x-cascade": true
}
[headers, page]
end,
"path" => "/"
}.each { |k, v| @properties[k] = v unless @properties[k] }
end
# Rack ingress point.
# This should not be called under any circumstances twice in the same application,
# although server nesting for the purpose of creating virtual hosts is allowed.
# @param env [Hash]
# @return [Array(Integer,Hash,Array)]
def call(env)
request = Landline::Request.new(env)
response = catch(:finish) do
go(request)
end
request.run_postprocessors(response)
Response.convert(response).finalize
end
end
end

52
lib/landline/util/jwt.rb

@ -14,10 +14,32 @@ module Landline
module Util
# JSON Web Token construction class
class JWT
ALGO = {
"HS256" => proc do |data, secret|
Base64.urlsafe_encode64(
OpenSSL::HMAC.digest("SHA256", secret, data)
).gsub('=', '')
end,
"HS384" => proc do |data, secret|
Base64.urlsafe_encode64(
OpenSSL::HMAC.digest("SHA384", secret, data)
).gsub('=', '')
end,
"HS512" => proc do |data, secret|
Base64.urlsafe_encode64(
OpenSSL::HMAC.digest("SHA512", secret, data)
).gsub('=', '')
end
}.freeze
# 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")
def initialize(data, halgo = "HS256")
unless ALGO.include? halgo
raise StandardError, "hash algorithm #{halgo} not supported"
end
@halgo = halgo
@data = data
end
@ -26,14 +48,13 @@ module Landline
# @param key [String]
# @return [String]
def make(key)
jsonheader = {
"alg": @halgo,
"typ": "JWT"
}.to_json
jsondata = @data.to_json
[
{
"hash" => @halgo
}.to_json,
jsondata,
OpenSSL::HMAC.digest(@halgo, key, jsondata)
].map(&Base64.method(:strict_encode64)).map(&:strip).join "&"
data = "#{base64(jsonheader)}.#{base64(jsondata)}"
"#{data}.#{ALGO[@halgo].call(data, key)}"
end
# Construct an object from string
@ -41,14 +62,21 @@ module Landline
# @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
halgoj, dataj, sig = input.split(".")
halgo = JSON.parse(Base64.urlsafe_decode64(halgoj))["alg"]
return nil unless ALGO.include? halgo
return nil if ALGO[halgo].call("#{halgoj}.#{dataj}", key) != sig
new(JSON.parse(dataj), halgo)
new(JSON.parse(Base64.urlsafe_decode64(dataj)), halgo)
end
attr_accessor :data
private
def base64(data)
Base64.urlsafe_encode64(data).gsub("=", "")
end
end
end
end

1
lib/landline/util/mime.rb

@ -1267,6 +1267,7 @@ module Landline
}.freeze
# Get MIME type by file extension
# @note This function does no checks on the file - simply renaming the file to a different extension will yield an invalid result. Do not use this to check uploaded files - preferably, use libmagic or proper mime type tools for Ruby.
# @param file [String] filename
# @return [String] MIME type, defaults to "application/octet-stream"
def self.get_mime_type(file)

2
lib/landline/util/query.rb

@ -8,6 +8,8 @@ module Landline
# Query string parser
class Query
include Landline::Util::ParserSorting
attr_reader :query
# @param query [String]
def initialize(query)
@query = query

Loading…
Cancel
Save