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
(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
23 changed files with 683 additions and 43 deletions
-
53examples/longpolling.ru
-
43examples/partial_hijacking.ru
-
73examples/rack_app.ru
-
67examples/session.ru
-
2landline.gemspec
-
1lib/landline.rb
-
50lib/landline/app.rb
-
21lib/landline/dsl/constructors_path.rb
-
2lib/landline/dsl/constructors_probe.rb
-
2lib/landline/dsl/methods_common.rb
-
17lib/landline/dsl/methods_path.rb
-
78lib/landline/dsl/methods_probe.rb
-
1lib/landline/dsl/methods_template.rb
-
97lib/landline/extensions/session.rb
-
14lib/landline/path.rb
-
1lib/landline/probe.rb
-
25lib/landline/probe/crosscall_handler.rb
-
61lib/landline/request.rb
-
2lib/landline/response.rb
-
61lib/landline/server.rb
-
52lib/landline/util/jwt.rb
-
1lib/landline/util/mime.rb
-
2lib/landline/util/query.rb
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue