(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)

This commit is contained in:
Yessiest 2024-04-27 17:23:34 +04:00
parent 1e546aa417
commit e251d2def6
23 changed files with 683 additions and 43 deletions

53
examples/longpolling.ru Normal file
View File

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

View File

@ -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 Normal file
View File

@ -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 Normal file
View File

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

View File

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

View File

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

50
lib/landline/app.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,12 +10,14 @@ module Landline
module DSL module DSL
# Common methods for Probe objects # Common methods for Probe objects
module ProbeMethods module ProbeMethods
# (in Landline::Probe context)
# Get the current request # Get the current request
# @return [Landline::Request] # @return [Landline::Request]
def request def request
@origin.request @origin.request
end end
# (in Landline::Probe context)
# Set response status (generate response if one doesn't exist yet) # Set response status (generate response if one doesn't exist yet)
# @param status [Integer] http status code # @param status [Integer] http status code
def status(status) def status(status)
@ -23,8 +25,59 @@ module Landline
@origin.response.status = status @origin.response.status = status
end 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 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) # Set response header (generate response if one doesn't exist yet)
# @param key [String] header name # @param key [String] header name
# @param value [String] header value # @param value [String] header value
@ -40,10 +93,11 @@ module Landline
end end
@origin.response = (@origin.response or Landline::Response.new) @origin.response = (@origin.response or Landline::Response.new)
key = key.downcase key = key.downcase.to_s
@origin.response.add_header(key, value) @origin.response.add_header(key, value)
end end
# (in Landline::Probe context)
# Delete a header value from the headers hash # Delete a header value from the headers hash
# If no value is provided, deletes all key entries # If no value is provided, deletes all key entries
# @param key [String] header name # @param key [String] header name
@ -61,9 +115,10 @@ module Landline
raise ArgumentError, "value key has invalid characters" raise ArgumentError, "value key has invalid characters"
end end
@origin.response.delete_header(key, value) @origin.response.delete_header(key.to_s, value)
end end
# (in Landline::Probe context)
# Set response cookie # Set response cookie
# @see Landline::Cookie.new # @see Landline::Cookie.new
def cookie(*params, **options) def cookie(*params, **options)
@ -73,6 +128,7 @@ module Landline
) )
end end
# (in Landline::Probe context)
# Delete a cookie # Delete a cookie
# If no value is provided, deletes all cookies with the same key # If no value is provided, deletes all cookies with the same key
# @param key [String] cookie key # @param key [String] cookie key
@ -83,6 +139,7 @@ module Landline
@origin.response.delete_cookie(key, value) @origin.response.delete_cookie(key, value)
end end
# (in Landline::Probe context)
# 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?
@ -90,6 +147,7 @@ module Landline
!!(value && opts && opts['boundary']) !!(value && opts && opts['boundary'])
end end
# (in Landline::Probe context)
# Returns formdata # Returns formdata
# @note reads request.input - may nullify request.body. # @note reads request.input - may nullify request.body.
# @return [Hash{String=>(String,Landline::Util::FormPart)}] # @return [Hash{String=>(String,Landline::Util::FormPart)}]
@ -102,12 +160,14 @@ module Landline
).to_h ).to_h
end end
# (in Landline::Probe context)
# Checks if current request has urlencoded query string # Checks if current request has urlencoded query string
# @return [Boolean] # @return [Boolean]
def query? def query?
!!_verify_content_type("application/x-www-form-urlencode") !!_verify_content_type("application/x-www-form-urlencode")
end end
# (in Landline::Probe context)
# Returns parsed query hash # Returns parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized # @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data # @return [Hash{String => Object}] query data
@ -115,6 +175,7 @@ module Landline
Landline::Util::Query.new(request.body).parse Landline::Util::Query.new(request.body).parse
end end
# (in Landline::Probe context)
# Returns shallow parsed query hash # Returns shallow parsed query hash
# @note reads request.body - may nullify .input, .body data is memoized # @note reads request.body - may nullify .input, .body data is memoized
# @return [Hash{String => Object}] query data # @return [Hash{String => Object}] query data
@ -122,12 +183,14 @@ module Landline
Landline::Util::Query.new(request.body).parse_shallow Landline::Util::Query.new(request.body).parse_shallow
end end
# (in Landline::Probe context)
# Check if body is a JSON object # Check if body is a JSON object
# @return [Boolean] # @return [Boolean]
def json? def json?
!!_verify_content_type('application/json') !!_verify_content_type('application/json')
end end
# (in Landline::Probe context)
# Return parse JSON object # Return parse JSON object
# @note reads request.input - may nullify request.body. # @note reads request.input - may nullify request.body.
# @return [Object] # @return [Object]
@ -135,24 +198,35 @@ module Landline
JSON.parse(request.input) JSON.parse(request.input)
end end
# (in Landline::Probe context)
# 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)
File.open("#{request.filepath}/#{path}", mode, *all, &block) File.open("#{request.filepath}/#{path}", mode, *all, &block)
end end
# (in Landline::Probe context)
# Escape HTML entities # Escape HTML entities
# @see Landline::Util.escape_html # @see Landline::Util.escape_html
def escape_html(text) def escape_html(text)
Landline::Util.escape_html(text) Landline::Util.escape_html(text)
end end
# (in Landline::Probe context)
# Unescape HTML entities # Unescape HTML entities
# @see Landline::Util.escape_html # @see Landline::Util.escape_html
def unescape_html(text) def unescape_html(text)
Landline::Util.unescape_html(text) Landline::Util.unescape_html(text)
end 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 private
def _verify_content_type(type) def _verify_content_type(type)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,10 +21,10 @@ module Landline
@param = {} @param = {}
@splat = [] @splat = []
# Traversal route. Public and writable. # 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. # File serving path. Public and writable.
@filepath = "/" @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) @rack = init_rack_vars(env)
# Internal navigation states. Private. # Internal navigation states. Private.
@states = [] @states = []
@ -35,9 +35,10 @@ module Landline
# Run postprocessors # Run postprocessors
# @param response [Landline::Response] # @param response [Landline::Response]
def run_postprocessors(response) def run_postprocessors(response)
@postprocessors.each do |postproc| @postprocessors.reverse_each do |postproc|
postproc.call(self, response) postproc.call(self, response)
end end
@postprocessors = []
end end
# Returns request body (if POST data exists) # Returns request body (if POST data exists)
@ -64,10 +65,33 @@ module Landline
@path, @param, @splat, @filepath = @states.pop @path, @param, @splat, @filepath = @states.pop
end 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, attr_reader :request_method, :script_name, :path_info, :server_name,
:server_port, :server_protocol, :headers, :param, :splat, :server_port, :server_protocol, :headers, :param, :splat,
:postprocessors, :query, :cookies :postprocessors, :cookies, :rack
attr_accessor :path, :filepath attr_accessor :path, :filepath, :query
private private
@ -119,7 +143,7 @@ module Landline
.freeze .freeze
end end
# Iniitalize headers hash # Initialize headers hash
# @param env [Hash] # @param env [Hash]
# @return Hash # @return Hash
def init_headers(env) def init_headers(env)
@ -133,5 +157,30 @@ module Landline
x.downcase.gsub("_", "-") if x.is_a? String x.downcase.gsub("_", "-") if x.is_a? String
end.freeze end.freeze
end 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
end end

View File

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

View File

@ -11,39 +11,64 @@ module Landline
# A specialized path that can be used directly as a Rack application. # A specialized path that can be used directly as a Rack application.
class Server < Landline::Path class Server < Landline::Path
Context = ServerContext Context = ServerContext
# @param parent [Landline::Node, nil] Parent object to inherit properties to # @param parent [Landline::Node, nil] Parent object to inherit properties to
# @param setup [#call] Setup block # @param setup [#call] Setup block
def initialize(parent: nil, **args, &setup) def initialize(passthrough = nil, parent: nil, **opts, &setup)
super("", parent: nil, **args, &setup) super("", parent: nil, **opts, &setup)
return if parent 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" => [], "index" => [],
"handle.default" => proc do |code, backtrace: nil| "handle.default" => proc do |code, backtrace: nil|
page = Landline::Util.default_error_page(code, backtrace) page = Landline::Util.default_error_page(code, backtrace)
headers = { headers = {
"content-length": page.bytesize, "content-length": page.bytesize,
"content-type": "text/html" "content-type": "text/html",
"x-cascade": true
} }
[headers, page] [headers, page]
end, end,
"path" => "/" "path" => "/"
}.each { |k, v| @properties[k] = v unless @properties[k] } }.each { |k, v| @properties[k] = v unless @properties[k] }
end 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
end end

View File

@ -14,10 +14,32 @@ module Landline
module Util module Util
# JSON Web Token construction class # JSON Web Token construction class
class JWT 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 # Create a new JWT token wrapper
# @param data [Hash, Array] JSON-formattable data # @param data [Hash, Array] JSON-formattable data
# @param halgo [String] Name of the hash algorithm to use # @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 @halgo = halgo
@data = data @data = data
end end
@ -26,14 +48,13 @@ module Landline
# @param key [String] # @param key [String]
# @return [String] # @return [String]
def make(key) def make(key)
jsonheader = {
"alg": @halgo,
"typ": "JWT"
}.to_json
jsondata = @data.to_json jsondata = @data.to_json
[ data = "#{base64(jsonheader)}.#{base64(jsondata)}"
{ "#{data}.#{ALGO[@halgo].call(data, key)}"
"hash" => @halgo
}.to_json,
jsondata,
OpenSSL::HMAC.digest(@halgo, key, jsondata)
].map(&Base64.method(:strict_encode64)).map(&:strip).join "&"
end end
# Construct an object from string # Construct an object from string
@ -41,14 +62,21 @@ module Landline
# @param key [String] # @param key [String]
# @return [JWT, nil] returns nil if verification couldn't complete # @return [JWT, nil] returns nil if verification couldn't complete
def self.from_string(input, key) def self.from_string(input, key)
halgoj, dataj, sig = input.split("&").map(&Base64.method(:strict_decode64)) halgoj, dataj, sig = input.split(".")
halgo = JSON.parse(halgoj)["hash"] halgo = JSON.parse(Base64.urlsafe_decode64(halgoj))["alg"]
return nil if OpenSSL::HMAC.digest(halgo, key, dataj) != sig 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 end
attr_accessor :data attr_accessor :data
private
def base64(data)
Base64.urlsafe_encode64(data).gsub("=", "")
end
end end
end end
end end

View File

@ -1267,6 +1267,7 @@ module Landline
}.freeze }.freeze
# Get MIME type by file extension # 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 # @param file [String] filename
# @return [String] MIME type, defaults to "application/octet-stream" # @return [String] MIME type, defaults to "application/octet-stream"
def self.get_mime_type(file) def self.get_mime_type(file)

View File

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