(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:
parent
1e546aa417
commit
e251d2def6
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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/**/*"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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…
Reference in New Issue