leanweb 0.1.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/leanweb +21 -16
- data/contrib/lib/hawese.rb +62 -0
- data/contrib/lib/lean_mail.rb +125 -0
- data/contrib/lib/leanweb/controller_mixins/render_with_layout.rb +170 -0
- data/contrib/lib/rack/session/json_file.rb +84 -0
- data/lib/leanweb/app.rb +18 -30
- data/lib/leanweb/controller.rb +56 -54
- data/lib/leanweb/helpers.rb +34 -0
- data/lib/leanweb/route.rb +90 -52
- data/lib/leanweb/version.rb +13 -0
- data/lib/leanweb.rb +13 -7
- metadata +118 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4e80c5813858b838b39fea56bb96b2fb28f7749d22f02ab04694662a8595d96
|
4
|
+
data.tar.gz: c2fdc8ebf92d647516db11352e5a6cb7bc36f1489dfabe69c095e7090f1b7970
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bed8458a93a2f0139609f17fe3402f69065182607b6b6664cc0c6d90ab3815f4dca7cd08c165dab17e5448d9c3f7d3c44ef8e1f28468cd3e4c42a66b096c8b8
|
7
|
+
data.tar.gz: 320c619aedd70c9bd0ed5e698ef456e293d3cf754bbe3ca939a7669b45ad117a371089db15737edf0c5c271ad638b84784e6ee3a730093a82a16843828a1074c
|
data/bin/leanweb
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
|
4
4
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
5
5
|
#
|
6
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
7
|
-
# General Public License version
|
8
|
-
# should have received a copy of this
|
9
|
-
# see <https://
|
6
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
7
|
+
# General Public License version 3 with permissions for compatibility with the
|
8
|
+
# Hacktivista General Public License. You should have received a copy of this
|
9
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
10
10
|
|
11
11
|
require 'fileutils'
|
12
12
|
require 'leanweb'
|
@@ -22,6 +22,9 @@ files = [
|
|
22
22
|
source 'https://rubygems.org'
|
23
23
|
|
24
24
|
gem 'leanweb', '~> #{LeanWeb::VERSION}'
|
25
|
+
gem 'haml', '~> 5.2.2'
|
26
|
+
gem 'rake'
|
27
|
+
gem 'puma'
|
25
28
|
RUBY
|
26
29
|
}, {
|
27
30
|
filename: 'config.ru',
|
@@ -29,21 +32,21 @@ files = [
|
|
29
32
|
# frozen_string_literal: true
|
30
33
|
|
31
34
|
require 'leanweb'
|
35
|
+
require_relative 'routes'
|
32
36
|
|
33
37
|
if ENV['RACK_ENV'] == 'development'
|
34
38
|
use(Rack::Reloader, 0)
|
35
|
-
use(Rack::Static, urls: [''], root: 'public', cascade: true)
|
39
|
+
use(Rack::Static, urls: ['/assets'], root: 'public', cascade: true)
|
36
40
|
end
|
37
41
|
|
38
|
-
|
39
|
-
run app
|
42
|
+
run LeanWeb::App.new(ROUTES)
|
40
43
|
RUBY
|
41
44
|
}, {
|
42
45
|
filename: 'routes.rb',
|
43
46
|
content: <<~RUBY
|
44
47
|
# frozen_string_literal: true
|
45
48
|
|
46
|
-
[{ path: '/' }]
|
49
|
+
ROUTES = [{ path: '/' }].freeze
|
47
50
|
RUBY
|
48
51
|
}, {
|
49
52
|
filename: 'Rakefile',
|
@@ -52,23 +55,24 @@ files = [
|
|
52
55
|
|
53
56
|
require 'leanweb'
|
54
57
|
|
55
|
-
task default: %w[
|
58
|
+
task default: %w[build_static]
|
56
59
|
|
57
|
-
task :
|
58
|
-
|
60
|
+
task :build_static do
|
61
|
+
require_relative 'routes'
|
62
|
+
LeanWeb::App.new(ROUTES).build_static
|
59
63
|
end
|
60
64
|
RUBY
|
61
65
|
}, {
|
62
|
-
filename: 'src/controllers/
|
66
|
+
filename: 'src/controllers/main_controller.rb',
|
63
67
|
content: <<~RUBY
|
64
68
|
# frozen_string_literal: true
|
65
69
|
|
66
70
|
require 'leanweb'
|
67
71
|
|
68
|
-
# Main controller is the default controller
|
72
|
+
# Main controller is the default controller.
|
69
73
|
class MainController < LeanWeb::Controller
|
70
74
|
def index_get
|
71
|
-
|
75
|
+
render_response 'index.haml'
|
72
76
|
end
|
73
77
|
end
|
74
78
|
RUBY
|
@@ -96,9 +100,10 @@ begin
|
|
96
100
|
path = ARGV[1] || '.'
|
97
101
|
FileUtils.mkdir_p(path)
|
98
102
|
FileUtils.cd(path)
|
99
|
-
FileUtils.mkdir_p(['public', 'src/controllers', 'src/views'])
|
100
|
-
files.each
|
103
|
+
FileUtils.mkdir_p(['public/assets', 'src/controllers', 'src/views'])
|
104
|
+
files.each{ |file| File.write(file[:filename], file[:content]) }
|
101
105
|
`git init`
|
106
|
+
`bundle install`
|
102
107
|
puts("Project '#{File.basename(Dir.getwd)}' successfully created.")
|
103
108
|
rescue # rubocop:disable Style/RescueStandardError
|
104
109
|
puts('Woops! Something went wrong.')
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'json'
|
11
|
+
require 'net/http'
|
12
|
+
require 'openssl'
|
13
|
+
require 'tilt'
|
14
|
+
require 'uri'
|
15
|
+
|
16
|
+
# Hawese client.
|
17
|
+
#
|
18
|
+
# **Environment variables**
|
19
|
+
#
|
20
|
+
# - HAWESE_RETURN_URL defaults to `LEANWEB_ENDPOINT/checkout`.
|
21
|
+
# - HAWESE_ENDPOINT
|
22
|
+
# - HAWESE_ORIGIN
|
23
|
+
class Hawese
|
24
|
+
class << self
|
25
|
+
ENDPOINT = ENV.fetch('HAWESE_ENDPOINT')
|
26
|
+
RETURN_URL = ENV.fetch('HAWESE_RETURN_URL') do
|
27
|
+
"#{ENV.fetch('LEANWEB_ENDPOINT')}/checkout"
|
28
|
+
end
|
29
|
+
ORIGIN = ENV.fetch('HAWESE_ORIGIN')
|
30
|
+
|
31
|
+
def payment_methods
|
32
|
+
uri = URI("#{ENDPOINT}/gateways/payment-methods/purchase")
|
33
|
+
JSON.parse(Net::HTTP.get(uri))
|
34
|
+
end
|
35
|
+
|
36
|
+
def gateway_schema(gateway, query_params, schema = 'purchase')
|
37
|
+
uri = URI("#{ENDPOINT}/gateways/#{gateway}/schemas/#{schema}")
|
38
|
+
uri.query = URI.encode_www_form(query_params)
|
39
|
+
JSON.parse(Net::HTTP.get(uri))
|
40
|
+
end
|
41
|
+
|
42
|
+
def purchase(gateway, fields)
|
43
|
+
fields[:origin] = ORIGIN
|
44
|
+
uri = URI("#{ENDPOINT}/gateways/#{gateway}/purchase")
|
45
|
+
uri.query = URI.encode_www_form(return_url: RETURN_URL)
|
46
|
+
response = Net::HTTP.post(
|
47
|
+
uri,
|
48
|
+
fields.to_json,
|
49
|
+
'Content-Type' => 'application/json'
|
50
|
+
)
|
51
|
+
JSON.parse(response.body)
|
52
|
+
end
|
53
|
+
|
54
|
+
def purchase_from_schema(gateway, schema)
|
55
|
+
fields = {}
|
56
|
+
schema['properties'].each do |property, contents|
|
57
|
+
fields[property] = contents['default'] if contents.include?('default')
|
58
|
+
end
|
59
|
+
purchase(gateway, fields)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'net/smtp'
|
11
|
+
require 'securerandom'
|
12
|
+
require 'socket'
|
13
|
+
require 'time'
|
14
|
+
|
15
|
+
# Send an email with SMTP easily.
|
16
|
+
#
|
17
|
+
# Environment variables:
|
18
|
+
#
|
19
|
+
# - SMTP_HOST: Where to connect to.
|
20
|
+
# - SMTP_PORT: Port, optional (default: 25).
|
21
|
+
# - SMTP_SECURITY: `tls` or `starttls`, optional (default: `nil`).
|
22
|
+
# - SMTP_USER: User, optional.
|
23
|
+
# - SMTP_PASSWORD: Password, optional.
|
24
|
+
# - SMTP_FROM: In the format `Name <user@mail>` or `user@mail`.
|
25
|
+
#
|
26
|
+
# @example Single address
|
27
|
+
# LeanMail.deliver('to@mail', 'subject', 'body')
|
28
|
+
#
|
29
|
+
# @example Multiple addresses
|
30
|
+
# LeanMail.deliver(['to@mail', 'to2@mail'], 'subject', 'body')
|
31
|
+
module LeanMail
|
32
|
+
# RFC 2821 message data representation.
|
33
|
+
class Data
|
34
|
+
attr_reader :from, :to, :subject, :message
|
35
|
+
|
36
|
+
def initialize(from, to, subject, message)
|
37
|
+
@from = from
|
38
|
+
@to = to
|
39
|
+
@subject = subject
|
40
|
+
@message = message
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
<<~MAIL
|
45
|
+
From: #{@from}
|
46
|
+
To: #{to.instance_of?(Array) ? to.join(', ') : to}
|
47
|
+
Subject: #{subject}
|
48
|
+
Date: #{Time.new.rfc2822}
|
49
|
+
Message-Id: <#{SecureRandom.uuid}@#{Socket.gethostname}>
|
50
|
+
|
51
|
+
#{@message}
|
52
|
+
MAIL
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Email deliverer.
|
57
|
+
class Deliver
|
58
|
+
attr_reader :data
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@user = ENV.fetch('SMTP_USER', nil)
|
62
|
+
@password = ENV.fetch('SMTP_PASSWORD', nil)
|
63
|
+
@from = ENV.fetch('SMTP_FROM')
|
64
|
+
|
65
|
+
@host = ENV.fetch('SMTP_HOST')
|
66
|
+
@port = ENV.fetch('SMTP_PORT', 25)
|
67
|
+
@security = ENV.fetch('SMTP_SECURITY', nil)
|
68
|
+
|
69
|
+
initialize_smtp
|
70
|
+
end
|
71
|
+
|
72
|
+
# Send email.
|
73
|
+
#
|
74
|
+
# @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
|
75
|
+
# @param subject [String]
|
76
|
+
# @param body [String]
|
77
|
+
# @return [Deliver]
|
78
|
+
def call(to, subject, body)
|
79
|
+
@data = Data.new(@from, to, subject, body)
|
80
|
+
|
81
|
+
@smtp.start(Socket.gethostname, @user, @password, :plain) do |smtp|
|
82
|
+
smtp.send_message(@data.to_s, extract_addr(@from), extract_addrs(to))
|
83
|
+
end
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def initialize_smtp
|
91
|
+
@smtp = Net::SMTP.new(@host, @port)
|
92
|
+
maybe_enable_security(@security)
|
93
|
+
end
|
94
|
+
|
95
|
+
def maybe_enable_security(security)
|
96
|
+
case security
|
97
|
+
when 'tls' then @smtp.enable_tls
|
98
|
+
when 'starttls' then @smtp.enable_starttls
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_addr(str)
|
103
|
+
match = str.match(%r{<([^/]+)>})
|
104
|
+
return match[1] if match
|
105
|
+
|
106
|
+
str
|
107
|
+
end
|
108
|
+
|
109
|
+
def extract_addrs(to)
|
110
|
+
return to.map{ |addr| extract_addr(addr) } if to.instance_of?(Array)
|
111
|
+
|
112
|
+
extract_addr(to)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Deliver email.
|
117
|
+
#
|
118
|
+
# @param to [Array, String] In the format `Name <user@mail>` or `user@mail`.
|
119
|
+
# @param subject [String]
|
120
|
+
# @param body [String]
|
121
|
+
# @return [Deliver]
|
122
|
+
def self.deliver(to, subject, body)
|
123
|
+
LeanMail::Deliver.new.call(to, subject, body)
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
|
11
|
+
module LeanWeb
|
12
|
+
module ControllerMixins
|
13
|
+
# {render_with_layout} for {Controller} with ERB, HAML, Tilt::EmacsOrg and
|
14
|
+
# Redcarpet Markdown support.
|
15
|
+
module RenderWithLayout
|
16
|
+
# Render a response with a layout.
|
17
|
+
#
|
18
|
+
# Depending on the `path` file contents and options `@content_for` might
|
19
|
+
# be filled. These contents must be inserted on the `head` or `body` of
|
20
|
+
# your layout file.
|
21
|
+
#
|
22
|
+
# All paths used on parameters can be relative to {VIEW_PATH} or absolute.
|
23
|
+
#
|
24
|
+
# @param path [String] Path to file to rendered.
|
25
|
+
# @param title [String] `@content_for[:title]` content.
|
26
|
+
# @param head [Hash] Things to be inserted on `@content_for[:head]`.
|
27
|
+
# @option head [String|Array] :js Javascript file path(s).
|
28
|
+
# @option head [String|Array] :jsm Javascript module file path (s).
|
29
|
+
# @option head [String|Array] :css CSS file path(s).
|
30
|
+
# @option head [String|Array] :raw Raw headers in HTML format.
|
31
|
+
# @param layout [String] Layout file path.
|
32
|
+
# @param sub_layout [String] Layout file to be inserted within layout.
|
33
|
+
# @param options [Hash] Options hash, to be given to the template engine
|
34
|
+
# based on file extension: `Tilt::EmacsOrg` for `.org` and `Redcarpet`
|
35
|
+
# for `.md`.
|
36
|
+
# @option options [String] :setupfile For `.org`.
|
37
|
+
# @option options [true] :toc For `.md`. To fill `@content_for[:toc]`
|
38
|
+
# render with option `:with_toc_data`.
|
39
|
+
# @yield On dynamic templates (such as Haml and ERB) you can yield.
|
40
|
+
def render_with_layout( # rubocop:disable Metrics/ParameterLists
|
41
|
+
path,
|
42
|
+
title: nil,
|
43
|
+
head: nil,
|
44
|
+
layout: 'layout.haml',
|
45
|
+
sub_layout: nil,
|
46
|
+
options: {},
|
47
|
+
&block
|
48
|
+
)
|
49
|
+
@content_for[:title] = title unless title.nil?
|
50
|
+
content = render_by_extension(path, sub_layout, options, &block)
|
51
|
+
prepare_head(head) unless head.nil?
|
52
|
+
render_response(layout, 'text/html'){ content }
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# @param path [String]
|
58
|
+
# @param layout [String]
|
59
|
+
# @param options [Hash] Check `Tilt::EmacsOrg` options.
|
60
|
+
# @option options [String] :setupfile can be absolute or relative to
|
61
|
+
# {VIEW_PATH}.
|
62
|
+
def render_org(path, layout: nil, options: {})
|
63
|
+
options[:setupfile] = "#{VIEW_PATH}/#{options[:setupfile]}" \
|
64
|
+
if options.include?(:setupfile) && options[:setupfile][0] != '/'
|
65
|
+
|
66
|
+
org_template = create_template(path, options)
|
67
|
+
|
68
|
+
@content_for[:title] = org_template.title \
|
69
|
+
unless @content_for.include?(:title)
|
70
|
+
|
71
|
+
if layout
|
72
|
+
create_template(layout).render(self){ org_template.render(self) }
|
73
|
+
else
|
74
|
+
org_template.render(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param path [String]
|
79
|
+
# @param layout [String]
|
80
|
+
# @param options [Hash] Check `RedCarpet` `render_options`.
|
81
|
+
# @option options [true] :toc To place an HTML based table of contents on
|
82
|
+
# `@content_for[:toc]` and render with option `:with_toc_data`.
|
83
|
+
def render_markdown(path, layout: nil, options: {})
|
84
|
+
maybe_render_markdown_toc!(path, options)
|
85
|
+
markdown_template = create_template(path, options)
|
86
|
+
if layout
|
87
|
+
create_template(layout).render(self){ markdown_template.render(self) }
|
88
|
+
else
|
89
|
+
markdown_template.render(self)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# If `options[:toc]` is set, deletes options `:toc` and adds
|
94
|
+
# `:with_toc_data`. Also adds `@content_for[:toc]`.
|
95
|
+
def maybe_render_markdown_toc!(path, options)
|
96
|
+
return unless options.include?(:toc)
|
97
|
+
|
98
|
+
require('redcarpet')
|
99
|
+
@content_for[:toc] = create_template(
|
100
|
+
path, { renderer: Redcarpet::Render::HTML_TOC }
|
101
|
+
).render
|
102
|
+
|
103
|
+
options.delete(:toc)
|
104
|
+
options[:with_toc_data] = true
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param path [String]
|
108
|
+
# @param layout [String]
|
109
|
+
# @param options [Hash] Check `Haml` options.
|
110
|
+
# @yield If block given.
|
111
|
+
def render_other(path, layout: nil, options: {})
|
112
|
+
template = create_template(path, options)
|
113
|
+
if layout
|
114
|
+
create_template(layout).render(self) do
|
115
|
+
template.render(self){ yield if block_given? }
|
116
|
+
end
|
117
|
+
else
|
118
|
+
template.render(self){ yield if block_given? }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_by_extension(path, layout, options, &block)
|
123
|
+
case File.extname(path)
|
124
|
+
when '.org'
|
125
|
+
render_org(path, layout: layout, options: options)
|
126
|
+
when '.md'
|
127
|
+
render_markdown(path, layout: layout, options: options)
|
128
|
+
else
|
129
|
+
render_other(path, layout: layout, options: options, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def prepare_head(js: nil, jsm: nil, css: nil, raw: nil)
|
134
|
+
head = String.new
|
135
|
+
head << prepare_head_js(js) unless js.nil?
|
136
|
+
head << prepare_head_js(jsm, 'module') unless jsm.nil?
|
137
|
+
head << prepare_head_css(css) unless css.nil?
|
138
|
+
head << prepare_head_raw(raw) unless raw.nil?
|
139
|
+
@content_for[:head] = head
|
140
|
+
end
|
141
|
+
|
142
|
+
def prepare_head_js(js, type = 'application/javascript')
|
143
|
+
head = String.new
|
144
|
+
js = [js] if js.instance_of?(String)
|
145
|
+
js.each do |src|
|
146
|
+
head << "<script type='#{type}' src='#{src}'"
|
147
|
+
head << ' defer' if type != 'module'
|
148
|
+
head << "></script>\n"
|
149
|
+
end
|
150
|
+
head
|
151
|
+
end
|
152
|
+
|
153
|
+
def prepare_head_css(css)
|
154
|
+
head = String.new
|
155
|
+
css = [css] if css.instance_of?(String)
|
156
|
+
css.each do |src|
|
157
|
+
head << "<link rel='stylesheet' type='text/css' href='#{src}'>\n"
|
158
|
+
end
|
159
|
+
head
|
160
|
+
end
|
161
|
+
|
162
|
+
def prepare_head_raw(raw)
|
163
|
+
head = String.new
|
164
|
+
raw = [raw] if raw.instance_of?(String)
|
165
|
+
raw.each{ |src| head << "#{src}\n" }
|
166
|
+
head
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
|
+
#
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
|
+
|
10
|
+
require 'date'
|
11
|
+
require 'fileutils'
|
12
|
+
require 'json'
|
13
|
+
require 'rack/session/abstract/id'
|
14
|
+
|
15
|
+
module Rack
|
16
|
+
module Session
|
17
|
+
# A JSON File based session storage.
|
18
|
+
class JsonFile < Abstract::PersistedSecure
|
19
|
+
# Cleans session files that have not been used.
|
20
|
+
# @param to [Time|Date] Time from which file has not been accessed.
|
21
|
+
# Defaults to 00:00 hrs 1 month ago.
|
22
|
+
# @param dir [String] Directory from which to clean files. Defaults to
|
23
|
+
# `/tmp/rack.session`.
|
24
|
+
def self.clean(
|
25
|
+
to = Date.today.prev_month,
|
26
|
+
dir = "#{Dir.tmpdir}/rack.session"
|
27
|
+
)
|
28
|
+
to = to.to_time if to.instance_of?(Date)
|
29
|
+
Dir["#{dir}/*"].each do |file|
|
30
|
+
FileUtils.rm(file, force: true) if ::File.atime(file) < to
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param options [Hash] Accepts a :session_dir path which defaults to
|
35
|
+
# `/tmp/rack.session`.
|
36
|
+
def initialize(app, options = {})
|
37
|
+
super(
|
38
|
+
app,
|
39
|
+
{
|
40
|
+
secure: ENV['RACK_ENV'] != 'development', # HTTPS unless in dev env
|
41
|
+
same_site: 'Strict' # don't allow cross-site sessions
|
42
|
+
}.merge!(options)
|
43
|
+
)
|
44
|
+
@session_dir = "#{Dir.tmpdir}/#{@key}" || @default_options[:session_dir]
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Array] [self, Hash].
|
48
|
+
def find_session(_req, sid)
|
49
|
+
sid = generate_sid if sid.nil?
|
50
|
+
|
51
|
+
[sid, JSON.parse(::File.read("#{@session_dir}/#{sid}"))]
|
52
|
+
rescue Errno::ENOENT
|
53
|
+
[generate_sid, {}]
|
54
|
+
rescue JSON::ParserError
|
55
|
+
[sid, {}]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param sid [String] Only known ids are allowed, to avoid session
|
59
|
+
# fixation attacks.
|
60
|
+
# @return [self|false] session_id or false.
|
61
|
+
def write_session(_req, sid, session, _options)
|
62
|
+
return false unless ::File.exist?("#{@session_dir}/#{sid}")
|
63
|
+
|
64
|
+
::File.open("#{@session_dir}/#{sid}", 'w', 0o600) do |f|
|
65
|
+
f << JSON.generate(session)
|
66
|
+
end
|
67
|
+
sid
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [self|nil] new session id or nil if options[:drop].
|
71
|
+
def delete_session(_req, sid, options)
|
72
|
+
FileUtils.rm("#{@session_dir}/#{sid}", force: true)
|
73
|
+
options&.include?(:drop) ? nil : generate_sid
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_sid(*)
|
77
|
+
sid = super
|
78
|
+
FileUtils.mkdir_p(@session_dir, mode: 0o700)
|
79
|
+
::File.new("#{@session_dir}/#{sid}", 'w', 0o600).close
|
80
|
+
sid
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/leanweb/app.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
|
4
4
|
#
|
5
|
-
# This file is part of "LeanWeb" and licensed under the terms of the
|
6
|
-
# General Public License version
|
7
|
-
# should have received a copy of this
|
8
|
-
# see <https://
|
5
|
+
# This file is part of "LeanWeb" and licensed under the terms of the GNU Affero
|
6
|
+
# General Public License version 3 with permissions for compatibility with the
|
7
|
+
# Hacktivista General Public License. You should have received a copy of this
|
8
|
+
# license along with the software. If not, see <https://gnu.org/licenses/>.
|
9
9
|
|
10
10
|
require 'rack'
|
11
11
|
|
@@ -20,19 +20,15 @@ module LeanWeb
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# Entry point for dynamic routes (Rack).
|
23
|
+
#
|
23
24
|
# @param env [Hash] `env` for Rack.
|
24
25
|
def call(env)
|
25
26
|
request = Rack::Request.new(env)
|
26
|
-
route =
|
27
|
-
|
28
|
-
|
29
|
-
rescue TypeError
|
30
|
-
r[:path] == request.path
|
31
|
-
end
|
32
|
-
end
|
33
|
-
return [404, {}, ['Not found']] unless route_exists?(route)
|
27
|
+
route = find_route(request)
|
28
|
+
route = Route.new(**route) unless route.nil?
|
29
|
+
return [404, {}, ['Not found']] unless route&.available?
|
34
30
|
|
35
|
-
|
31
|
+
route.respond(request)
|
36
32
|
end
|
37
33
|
|
38
34
|
# Build static routes to files.
|
@@ -42,31 +38,23 @@ module LeanWeb
|
|
42
38
|
next unless route.static
|
43
39
|
|
44
40
|
begin
|
45
|
-
route.static.each
|
41
|
+
route.static.each{ |seed| route.build(route.seed_path(seed)) }
|
46
42
|
rescue NoMethodError
|
47
43
|
route.build
|
48
44
|
end
|
49
45
|
end
|
50
46
|
end
|
51
47
|
|
52
|
-
# Initialize by evaluating the routes file.
|
53
|
-
# Do this here so users don't freak out for using eval and rubocop is happy
|
54
|
-
# on client side.
|
55
|
-
# @param file [String] Routes file.
|
56
|
-
def self.init(file = 'routes.rb')
|
57
|
-
new(eval(File.read(file))) # rubocop:disable Security/Eval
|
58
|
-
end
|
59
|
-
|
60
48
|
protected
|
61
49
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
50
|
+
def find_route(request)
|
51
|
+
@routes.find do |r|
|
52
|
+
(r[:method] || 'GET') == request.request_method && begin
|
53
|
+
r[:path] =~ request.path
|
54
|
+
rescue TypeError
|
55
|
+
r[:path] == request.path
|
56
|
+
end
|
57
|
+
end
|
70
58
|
end
|
71
59
|
end
|
72
60
|
end
|