voffie_teapot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/voffie_teapot/get_handler.rb +73 -0
- data/lib/voffie_teapot/http_handler.rb +51 -0
- data/lib/voffie_teapot/main.rb +131 -0
- data/lib/voffie_teapot/parser.rb +33 -0
- data/lib/voffie_teapot/post_handler.rb +18 -0
- data/lib/voffie_teapot/resource_manager.rb +55 -0
- data/lib/voffie_teapot/response.rb +82 -0
- data/lib/voffie_teapot/utils.rb +14 -0
- data/lib/voffie_teapot/version.rb +5 -0
- data/lib/voffie_teapot/views/404.html +54 -0
- data/lib/voffie_teapot/views/500.html +68 -0
- data/lib/voffie_teapot.rb +10 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3887b642309860f8590ab04c04c0e4c74717259fd4c6a294c72db4f219e1bed4
|
4
|
+
data.tar.gz: 993c9a2abdc2c34665b65cb74fbe192b4fb359f9cffc69a37bb3f01338ba5a3f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2397220b9073b29a4232a8481f16f6de00c189f82680a0268e6cbe8bfaf3ddfd67cd6439fe55c46abc947d3c0e840f91dec9f687574a86160b91d52bccb5ab97
|
7
|
+
data.tar.gz: 99d4343ab27b38565d0b5f515605581f31d01de9c9c725cd0d2cfb5c2c9decf9830410b1f97944f341cac6847446bbadb8d6be0e0705a0e1a1f87dc388d8a37a
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'voffie_teapot/response'
|
4
|
+
require 'voffie_teapot/resource_manager'
|
5
|
+
|
6
|
+
module VoffieTeapot
|
7
|
+
# GET implementation of HTTPHandler
|
8
|
+
class GetHandler < HTTPHandler
|
9
|
+
include ResourceManager
|
10
|
+
|
11
|
+
def handle(request)
|
12
|
+
if matches?(request[:resource])
|
13
|
+
route_params = extract_params(request[:resource])
|
14
|
+
request[:params] = route_params
|
15
|
+
|
16
|
+
response = Response.new
|
17
|
+
response.change_content_type('text/html; charset=utf-8')
|
18
|
+
begin
|
19
|
+
@block.call(request, response)
|
20
|
+
return response
|
21
|
+
rescue StandardError => e
|
22
|
+
return Response.default500(e)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
handle_static_resource(request)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def handle_static_resource(request)
|
32
|
+
if request[:Accept]&.include?('text/css')
|
33
|
+
handle_css(request)
|
34
|
+
elsif IMG_CONTENT_TYPES.keys.include?(request[:resource].split('.').last.to_sym)
|
35
|
+
handle_image(request)
|
36
|
+
elsif request[:resource].end_with?('.js', '.ts')
|
37
|
+
handle_script(request)
|
38
|
+
else
|
39
|
+
Response.default404(request[:resounce])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle_css(request)
|
44
|
+
value = css(request[:resource])
|
45
|
+
if value[:status] == 200
|
46
|
+
Response.new(200, value[:file], { 'Content-Type' => 'text/css' })
|
47
|
+
else
|
48
|
+
Response.default404(request[:resource])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_image(request)
|
53
|
+
value = load_img(request[:resource])
|
54
|
+
if value[:status] == 200
|
55
|
+
response = Response.new(200, value[:file])
|
56
|
+
response.change_content_type(value[:ct])
|
57
|
+
response.change_content_length(value[:size])
|
58
|
+
response
|
59
|
+
else
|
60
|
+
Response.default404(request[:resource])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_script(request)
|
65
|
+
value = load_script(request[:resource])
|
66
|
+
if value[:status] == 200
|
67
|
+
Response.new(200, value[:file], { 'Content-Type' => '*/*' })
|
68
|
+
else
|
69
|
+
Response.default404(request[:resource])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'voffie_teapot/utils'
|
4
|
+
|
5
|
+
module VoffieTeapot
|
6
|
+
# Abstract class for HTTP method handlers
|
7
|
+
class HTTPHandler
|
8
|
+
include Utils
|
9
|
+
|
10
|
+
def initialize(path, block = nil)
|
11
|
+
@path = path
|
12
|
+
@regex = generate_reg_exp(path) if path
|
13
|
+
@block = block
|
14
|
+
@param_names = extract_param_names(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(path)
|
18
|
+
return false unless @regex
|
19
|
+
|
20
|
+
@regex.match?(path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_params(resource)
|
24
|
+
return {} unless resource.is_a?(String) && @path.is_a?(String)
|
25
|
+
|
26
|
+
param_names = extract_param_names(resource)
|
27
|
+
match_data = @regex.match(resource)
|
28
|
+
|
29
|
+
return {} unless param_names.any? && match_data
|
30
|
+
|
31
|
+
@param_names.zip(match_data.captures).to_h
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle(request)
|
35
|
+
raise NotImplementedError, 'You must implement the `handle` method in a subclass.'
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def extract_param_names(resource)
|
41
|
+
return [] unless resource.is_a?(String) && @path.is_a?(String)
|
42
|
+
|
43
|
+
path_segments = @path.split('/')
|
44
|
+
resource_segments = resource.split('/')
|
45
|
+
|
46
|
+
return [] if path_segments.size != resource_segments.size
|
47
|
+
|
48
|
+
path_segments.select { |segment| segment.start_with?(':') }.map { |param| param[1..].to_sym }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'json'
|
5
|
+
require 'voffie_teapot/parser'
|
6
|
+
require 'voffie_teapot/response'
|
7
|
+
require 'voffie_teapot/http_handler'
|
8
|
+
require 'voffie_teapot/get_handler'
|
9
|
+
require 'voffie_teapot/post_handler'
|
10
|
+
|
11
|
+
module VoffieTeapot
|
12
|
+
# Base class for Teapot gem
|
13
|
+
class Main
|
14
|
+
include Parser
|
15
|
+
|
16
|
+
def initialize(port)
|
17
|
+
@server = TCPServer.new(port)
|
18
|
+
@running = true
|
19
|
+
@routes = { 'GET' => [], 'POST' => [] }
|
20
|
+
@middleware = []
|
21
|
+
|
22
|
+
Signal.trap('INT') do
|
23
|
+
puts "\nShutting down server..."
|
24
|
+
close
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def close
|
29
|
+
@running = false
|
30
|
+
@server.close
|
31
|
+
end
|
32
|
+
|
33
|
+
def listen
|
34
|
+
puts "Teapot server started. Listening on port #{@server.addr[1]}..."
|
35
|
+
|
36
|
+
while @running
|
37
|
+
socket = @server.accept
|
38
|
+
Thread.new { handle_connection(socket) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(path, &block)
|
43
|
+
@routes['GET'] << GetHandler.new(path, block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def post(path, &block)
|
47
|
+
@routes['POST'] << PostHandler.new(path, block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def use(path, &middleware)
|
51
|
+
@middleware << { path: path, middleware: middleware }
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_middleware(request, response)
|
55
|
+
matching_middleware = @middleware.select { |m| match_route(m[:path], request[:resource]) }
|
56
|
+
|
57
|
+
matching_middleware.each do |middleware|
|
58
|
+
res = middleware[:middleware].call(request, response)
|
59
|
+
return res if res.is_a?(Response)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def match_route(route, resource)
|
64
|
+
return true if route == '*'
|
65
|
+
|
66
|
+
route_segments = route.split('/')
|
67
|
+
resource_segments = resource.split('/')
|
68
|
+
|
69
|
+
return false if route_segments.length != resource_segments.length
|
70
|
+
|
71
|
+
route_segments.each_with_index do |_segments, index|
|
72
|
+
if segment.start_with?(':')
|
73
|
+
next
|
74
|
+
elsif segment != resource_segments[index]
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_connection(socket)
|
83
|
+
request = read_request(socket)
|
84
|
+
return unless request
|
85
|
+
|
86
|
+
parsed_request = parse(request)
|
87
|
+
if parsed_request[:Content_Length].to_i.positive?
|
88
|
+
parsed_request[:body] = JSON.parse(socket.read(parsed_request[:Content_Length].to_i))
|
89
|
+
parsed_request[:body] = parsed_request[:body].transform_keys(&:to_sym)
|
90
|
+
end
|
91
|
+
|
92
|
+
response = route_request(parsed_request).create_response
|
93
|
+
socket.print response
|
94
|
+
socket.close
|
95
|
+
end
|
96
|
+
|
97
|
+
def read_request(socket)
|
98
|
+
request = ''
|
99
|
+
while (line = socket.gets) && line !~ /^\s*$/
|
100
|
+
request += line
|
101
|
+
end
|
102
|
+
request.empty? ? nil : request
|
103
|
+
end
|
104
|
+
|
105
|
+
def route_request(request)
|
106
|
+
response = Response.new
|
107
|
+
process_middleware(request, response)
|
108
|
+
|
109
|
+
return response unless response.body == ''
|
110
|
+
|
111
|
+
method = request[:method]
|
112
|
+
return Response.default404(request[:path]) unless @routes.key?(method)
|
113
|
+
|
114
|
+
handler = @routes[method].find { |h| h.matches?(request[:resource]) }
|
115
|
+
if handler
|
116
|
+
response = handler.handle(request)
|
117
|
+
return response.is_a?(Response) ? response : Response.default404(request[:path])
|
118
|
+
end
|
119
|
+
|
120
|
+
response =
|
121
|
+
case method
|
122
|
+
when 'GET'
|
123
|
+
GetHandler.new(nil).handle(request)
|
124
|
+
when 'POST'
|
125
|
+
PostHandler.new(nil).handle(request)
|
126
|
+
end
|
127
|
+
|
128
|
+
response.is_a?(Response) ? response : Response.default404(request[:path])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
VALID_METHODS = %w[GET POST PUT DELETE PATCH].freeze
|
4
|
+
|
5
|
+
module VoffieTeapot
|
6
|
+
# Parser module to parse HTTP requests
|
7
|
+
module Parser
|
8
|
+
def parse(request)
|
9
|
+
lines = request.split("\n")
|
10
|
+
return nil if lines[0].nil?
|
11
|
+
|
12
|
+
method, resource, http = lines[0].split
|
13
|
+
parsed_request = {}
|
14
|
+
|
15
|
+
if VALID_METHODS.include?(method)
|
16
|
+
parsed_request[:method] = method
|
17
|
+
parsed_request[:resource] = resource
|
18
|
+
parsed_request[:http] = http
|
19
|
+
lines[1..].each do |row|
|
20
|
+
key, val = row.split(': ', 2)
|
21
|
+
next if key.nil? || val.nil?
|
22
|
+
|
23
|
+
parsed_request[key.gsub('-', '_').to_sym] = val.strip
|
24
|
+
end
|
25
|
+
else
|
26
|
+
parsed_request[:error] = "Invalid method: #{method}"
|
27
|
+
end
|
28
|
+
parsed_request
|
29
|
+
rescue StandardError => e
|
30
|
+
{ error: "Error parsing request: #{e.message}" }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VoffieTeapot
|
4
|
+
# POST implementation of HTTPHandler
|
5
|
+
class PostHandler < HTTPHandler
|
6
|
+
def handle(request)
|
7
|
+
return unless matches?(request[:resource])
|
8
|
+
|
9
|
+
response = Response.new
|
10
|
+
begin
|
11
|
+
@block.call(request, response)
|
12
|
+
rescue StandardError => e
|
13
|
+
return Response.default500(e)
|
14
|
+
end
|
15
|
+
response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'slim'
|
4
|
+
|
5
|
+
IMG_CONTENT_TYPES = {
|
6
|
+
apng: 'image/apng', avif: 'image/avif', gif: 'image/gif',
|
7
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', jfif: 'image/jpeg',
|
8
|
+
pjpeg: 'image/jpeg', pjp: 'image/jpeg', png: 'image/png',
|
9
|
+
svg: 'image/svg+xml', webp: 'image/webp'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
module VoffieTeapot
|
13
|
+
# Module to handle loading/generating resources
|
14
|
+
module ResourceManager
|
15
|
+
def load_slim(resource, locals = {}, layout: true)
|
16
|
+
content = read_file("./views/#{resource}.slim")
|
17
|
+
layout_content = layout ? read_file('./views/layout.slim') : nil
|
18
|
+
|
19
|
+
rendered = Slim::Template.new { content }.render(nil, locals)
|
20
|
+
layout ? Slim::Template.new { layout_content }.render { rendered } : rendered
|
21
|
+
rescue StandardError => e
|
22
|
+
"Error rendering template #{e.message}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def css(resource)
|
26
|
+
load_public_file(resource, 'text/css')
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_img(resource)
|
30
|
+
load_public_file(resource, IMG_CONTENT_TYPES[resource.split('.').last.to_sym])
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_script(resource)
|
34
|
+
load_public_file(resource, '*/*')
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def load_public_file(resource, content_type)
|
40
|
+
normalized_resource = resource.start_with?('/') ? resource[1..] : resource
|
41
|
+
path = "./public/#{normalized_resource}"
|
42
|
+
if File.file?(path)
|
43
|
+
{ file: File.read(path), ct: content_type, status: 200, size: File.size(path) }
|
44
|
+
else
|
45
|
+
{ status: 404 }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_file(path)
|
50
|
+
File.read(path)
|
51
|
+
rescue Errno::ENOENT
|
52
|
+
raise "File not found #{path}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'voffie_teapot/resource_manager'
|
4
|
+
|
5
|
+
module VoffieTeapot
|
6
|
+
# Response class to handle/generate responses
|
7
|
+
class Response
|
8
|
+
include ResourceManager
|
9
|
+
attr_accessor :body, :status, :header
|
10
|
+
|
11
|
+
def initialize(status = 200, body = '', header = {})
|
12
|
+
@status = status
|
13
|
+
@body = body
|
14
|
+
@header = header
|
15
|
+
@cookies = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default404(route)
|
19
|
+
body = load_error_page('404.html', { route: route })
|
20
|
+
new(404, body, { 'Content-Type' => 'text/html' })
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.default500(error)
|
24
|
+
body = load_error_page('500.html', { error_message: error.message, error_class: error.class, backtrace: error.backtrace })
|
25
|
+
new(500, body, { 'Content-Type' => 'text/html' })
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load_error_page(filename, locals = {})
|
29
|
+
user_path = "./views/#{filename}"
|
30
|
+
gem_path = File.join(Gem::Specification.find_by_name('voffie_teapot').gem_dir, 'lib', 'voffie_teapot', 'views', filename)
|
31
|
+
|
32
|
+
if File.exist?(user_path)
|
33
|
+
template = File.read(user_path)
|
34
|
+
else
|
35
|
+
return '<html><body><h1>Error loading template</h1></body></html>' unless File.exist?(gem_path)
|
36
|
+
|
37
|
+
template = File.read(gem_path)
|
38
|
+
end
|
39
|
+
|
40
|
+
locals.each do |key, value|
|
41
|
+
template.gsub!("<%= #{key} %>", value.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
template
|
45
|
+
rescue Errno::ENOENT
|
46
|
+
'<html><body><h1>Error loading template</h1></body></html>'
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_cookie(name, value)
|
50
|
+
@cookies[name] = value
|
51
|
+
end
|
52
|
+
|
53
|
+
def change_status(status)
|
54
|
+
@status = status
|
55
|
+
end
|
56
|
+
|
57
|
+
def change_content_type(type)
|
58
|
+
@header['Content-Type'] = type
|
59
|
+
end
|
60
|
+
|
61
|
+
def change_content_length(length)
|
62
|
+
@header['Content-Length'] = length.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_response
|
66
|
+
headers = @header.to_a.map { |key, value| "#{key}: #{value}" }.join("\r\n")
|
67
|
+
cookies = @cookies.to_a.map { |key, value| "Set-Cookie: #{key}=#{value}" }.join("\r\n")
|
68
|
+
response = "HTTP/1.1 #{@status}\r\n#{headers}\r\n"
|
69
|
+
response += "#{cookies}\r\n" unless cookies.empty?
|
70
|
+
response + "\r\n#{@body}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def redirect(location)
|
74
|
+
@header['Location'] = location
|
75
|
+
change_status(301)
|
76
|
+
end
|
77
|
+
|
78
|
+
def slim(resource, locals = {}, layout: true)
|
79
|
+
load_slim(resource, locals, layout: layout)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VoffieTeapot
|
4
|
+
# Module containing all utility methods required
|
5
|
+
module Utils
|
6
|
+
def generate_reg_exp(path)
|
7
|
+
return %r{^/$} if path == '/'
|
8
|
+
|
9
|
+
Regexp.new("^#{path.split('/').map do |segment|
|
10
|
+
segment.start_with?(':') ? '([^/]+)' : Regexp.escape(segment)
|
11
|
+
end.join('/')}$")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
|
4
|
+
<head>
|
5
|
+
<meta charset='UTF-8' />
|
6
|
+
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
7
|
+
<title>404 - Not Found</title>
|
8
|
+
<style>
|
9
|
+
body {
|
10
|
+
font-family: Arial, sans-serif;
|
11
|
+
background-color: #e0f2f1;
|
12
|
+
color: #004d40;
|
13
|
+
display: flex;
|
14
|
+
justify-content: center;
|
15
|
+
align-items: center;
|
16
|
+
height: 100vh;
|
17
|
+
margin: 0;
|
18
|
+
padding: 20px;
|
19
|
+
box-sizing: border-box;
|
20
|
+
}
|
21
|
+
|
22
|
+
.container {
|
23
|
+
text-align: center;
|
24
|
+
background-color: #fff;
|
25
|
+
border-radius: 8px;
|
26
|
+
padding: 40px;
|
27
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
28
|
+
max-width: 600px;
|
29
|
+
width: 100%;
|
30
|
+
}
|
31
|
+
|
32
|
+
h1 {
|
33
|
+
margin-top: 0;
|
34
|
+
color: #00796b;
|
35
|
+
}
|
36
|
+
|
37
|
+
.teapot {
|
38
|
+
font-size: 48px;
|
39
|
+
margin-bottom: 20px;
|
40
|
+
}
|
41
|
+
</style>
|
42
|
+
</head>
|
43
|
+
|
44
|
+
<body>
|
45
|
+
<div class="container">
|
46
|
+
<div class="teapot">🫖</div>
|
47
|
+
<h1>404 - Page Not Found</h1>
|
48
|
+
<p>Oh no! It seems you've stumbled upon an empty teacup. The page you're looking for has vanished like steam from a
|
49
|
+
hot brew.</p>
|
50
|
+
<p>Why not try steeping a fresh URL or returning to our <a href="/">homepage</a></p>
|
51
|
+
</div>
|
52
|
+
</body>
|
53
|
+
|
54
|
+
</html>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
|
4
|
+
<head>
|
5
|
+
<meta charset='UTF-8' />
|
6
|
+
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
7
|
+
<title>500 - Internal Server Error</title>
|
8
|
+
<style>
|
9
|
+
body {
|
10
|
+
font-family: Arial, sans-serif;
|
11
|
+
background-color: #f0e6d2;
|
12
|
+
color: #5d4037;
|
13
|
+
display: flex;
|
14
|
+
justify-content: center;
|
15
|
+
align-items: center;
|
16
|
+
height: 100vh;
|
17
|
+
margin: 0;
|
18
|
+
padding: 20px;
|
19
|
+
box-sizing: border-box;
|
20
|
+
}
|
21
|
+
|
22
|
+
.container {
|
23
|
+
text-align: center;
|
24
|
+
background-color: #fff;
|
25
|
+
border-radius: 8px;
|
26
|
+
padding: 40px;
|
27
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
28
|
+
max-width: 600px;
|
29
|
+
width: 100%;
|
30
|
+
}
|
31
|
+
|
32
|
+
h1 {
|
33
|
+
margin-top: 0;
|
34
|
+
color: #d32f2f;
|
35
|
+
}
|
36
|
+
|
37
|
+
.teapot {
|
38
|
+
font-size: 48px;
|
39
|
+
margin-bottom: 20px;
|
40
|
+
}
|
41
|
+
|
42
|
+
pre {
|
43
|
+
background-color: #f5f5f5;
|
44
|
+
padding: 15px;
|
45
|
+
border-radius: 4px;
|
46
|
+
overflow-x: auto;
|
47
|
+
text-align: left;
|
48
|
+
white-space: pre-line;
|
49
|
+
}
|
50
|
+
</style>
|
51
|
+
</head>
|
52
|
+
|
53
|
+
<body>
|
54
|
+
<div class="container">
|
55
|
+
<div class="teapot">🫖</div>
|
56
|
+
<h1>500 - Internal Server Error</h1>
|
57
|
+
<p>Oops! It seems our teapot has overflowed. We're mopping up the mess and will be back shortly.</p>
|
58
|
+
<p>Error details: <strong>
|
59
|
+
<%= error_class %> - <%= error_message %>
|
60
|
+
</strong>
|
61
|
+
</p>
|
62
|
+
<pre id="error-details">
|
63
|
+
<%= backtrace %>
|
64
|
+
</pre>
|
65
|
+
</div>
|
66
|
+
</body>
|
67
|
+
|
68
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: voffie_teapot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Voffie
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-01-27 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: slim
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '5.2'
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 5.2.1
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '5.2'
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 5.2.1
|
32
|
+
description: A web server using Ruby's built-in TCPServer class & web sockets
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- lib/voffie_teapot.rb
|
38
|
+
- lib/voffie_teapot/get_handler.rb
|
39
|
+
- lib/voffie_teapot/http_handler.rb
|
40
|
+
- lib/voffie_teapot/main.rb
|
41
|
+
- lib/voffie_teapot/parser.rb
|
42
|
+
- lib/voffie_teapot/post_handler.rb
|
43
|
+
- lib/voffie_teapot/resource_manager.rb
|
44
|
+
- lib/voffie_teapot/response.rb
|
45
|
+
- lib/voffie_teapot/utils.rb
|
46
|
+
- lib/voffie_teapot/version.rb
|
47
|
+
- lib/voffie_teapot/views/404.html
|
48
|
+
- lib/voffie_teapot/views/500.html
|
49
|
+
homepage: https://github.com/voffie/teapot
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata:
|
53
|
+
source_code_uri: https://github.com/voffie/teapot
|
54
|
+
bug_tracker_uri: https://github.com/voffie/teapot/issues
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.7.8
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.7.0.dev
|
70
|
+
specification_version: 4
|
71
|
+
summary: TCP based web server
|
72
|
+
test_files: []
|