pickles_http 0.0.1
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/pickles_http/logger.rb +35 -0
- data/lib/pickles_http/router.rb +23 -0
- data/lib/pickles_http/server.rb +156 -0
- data/lib/pickles_http/utils.rb +178 -0
- data/lib/pickles_http.rb +29 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a286f482b6a031c86c1466c37d0e53a21440e54f9b74345bab2861bd276514bf
|
4
|
+
data.tar.gz: 14600496b57a31c095f916241e413d664b209d5d255f5990795ac2a537ebd67c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 334a300e1e486e682b8790bfbf7159b323ff3b6e2574839168adca3d067657c40527bc40be254c43a60119a48f1c98baf34b13966c971e9994f653e441e8e661
|
7
|
+
data.tar.gz: 945c6e86a17683f23e523dcf118f325bd7935fe2f56ac4edeefec45bb7070f78dd3e63df66164b71e38a3f5091c78b99c0d648cf77b08a5cd4b8927a02e25b20
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'utils'
|
2
|
+
|
3
|
+
class PicklesHttpServer
|
4
|
+
class Logger
|
5
|
+
include PicklesHttpServer::Utils
|
6
|
+
|
7
|
+
def initialize(log_file, log_path: "./log.txt")
|
8
|
+
@log_file = File.open(log_path, 'a') if log_file
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_log_path(new_path)
|
12
|
+
@log_file.close if @log_file if @log_file
|
13
|
+
@log_file = File.open(new_path, 'a') if @log_file
|
14
|
+
end
|
15
|
+
|
16
|
+
def log(message, severity = LogMode::INFO)
|
17
|
+
severity_level = LogMode::SEVERITIES[severity.upcase] || LogMode::SEVERITIES[LogMode::INFO]
|
18
|
+
log_entry(severity_level, message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def close
|
22
|
+
@log_file.close if @log_file
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def log_entry(level, message)
|
28
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
29
|
+
log_entry = "[#{timestamp}] [#{LogMode::SEVERITIES.keys[level]}] #{message}\n"
|
30
|
+
puts log_entry
|
31
|
+
@log_file.puts(log_entry) if @log_file
|
32
|
+
@log_file.flush if @log_file
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class PicklesHttpServer
|
2
|
+
class Router
|
3
|
+
def initialize()
|
4
|
+
@routes = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_route(method, path, handler)
|
8
|
+
method = method.upcase
|
9
|
+
@routes[method] ||= {}
|
10
|
+
@routes[method][path] = handler
|
11
|
+
end
|
12
|
+
|
13
|
+
def route_request(method, path)
|
14
|
+
method = method.upcase
|
15
|
+
return nil unless @routes[method]
|
16
|
+
|
17
|
+
handler = @routes[method][path]
|
18
|
+
return nil unless handler
|
19
|
+
|
20
|
+
handler
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'http/parser'
|
5
|
+
require 'concurrent'
|
6
|
+
require_relative 'utils'
|
7
|
+
require_relative 'router'
|
8
|
+
require_relative 'logger'
|
9
|
+
|
10
|
+
READ_CHUNK = 1024 * 4
|
11
|
+
|
12
|
+
class PicklesHttpServer
|
13
|
+
class Server
|
14
|
+
include PicklesHttpServer::Utils
|
15
|
+
|
16
|
+
def initialize(port, log_file, host)
|
17
|
+
@port = port
|
18
|
+
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
|
19
|
+
|
20
|
+
addr = Socket.pack_sockaddr_in(port, '127.0.0.1')
|
21
|
+
@socket.bind(addr)
|
22
|
+
@socket.listen(Socket::SOMAXCONN)
|
23
|
+
@socket.setsockopt(:SOCKET, :REUSEADDR, true)
|
24
|
+
|
25
|
+
@router = Router.new
|
26
|
+
@logger = PicklesHttpServer::Logger.new(log_file)
|
27
|
+
@request_queue = SizedQueue.new(10)
|
28
|
+
@write_mutex = Mutex.new
|
29
|
+
@middlewares = []
|
30
|
+
@not_found_message = "404: Route not found"
|
31
|
+
end
|
32
|
+
|
33
|
+
def change_server_option(option, value)
|
34
|
+
@not_found_message = value if option == "set_default_not_found_message"
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_route(method, path, handler)
|
38
|
+
@router.add_route(method, path, handler)
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_middleware(middleware)
|
42
|
+
@middlewares << middleware
|
43
|
+
end
|
44
|
+
|
45
|
+
def start
|
46
|
+
puts "PicklesServer is running on http://localhost:#{@port} 🔥"
|
47
|
+
start_request_processing_thread
|
48
|
+
accept_and_process_requests
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def start_request_processing_thread
|
54
|
+
Thread.new do
|
55
|
+
loop do
|
56
|
+
req_queue = @request_queue.pop
|
57
|
+
handle_request(req_queue)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def accept_and_process_requests
|
63
|
+
loop do
|
64
|
+
client, addrinfo = @socket.accept
|
65
|
+
begin
|
66
|
+
request = client.readpartial(READ_CHUNK)
|
67
|
+
@request_queue.push({ client: client, request: request })
|
68
|
+
rescue EOFError => e
|
69
|
+
puts "Client closed the connection: #{e.message}"
|
70
|
+
puts e.backtrace.join("\n")
|
71
|
+
client.close
|
72
|
+
rescue StandardError => e
|
73
|
+
puts "Error in accept_and_process_requests: #{e.message}"
|
74
|
+
puts e.backtrace
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_request(request_queue)
|
80
|
+
client = request_queue.fetch(:client)
|
81
|
+
request = request_queue.fetch(:request)
|
82
|
+
|
83
|
+
method, path, version = request.lines[0].split
|
84
|
+
|
85
|
+
headers = read_headers(request)
|
86
|
+
body = request.lines[10..-1].join
|
87
|
+
|
88
|
+
req_parsed = Utils.parse_request(client, body, headers)
|
89
|
+
|
90
|
+
handler = @router.route_request(method, path)
|
91
|
+
promises = []
|
92
|
+
|
93
|
+
promises << Concurrent::Promises.future do
|
94
|
+
@write_mutex.synchronize do
|
95
|
+
Middlewares.catcher.call(client, method, path, @logger)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
@middlewares.each do |middleware|
|
100
|
+
promises << Concurrent::Promises.future do
|
101
|
+
@write_mutex.synchronize do
|
102
|
+
middleware.middleware.call(req_parsed, middleware.custom_headers)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
Concurrent::Promises.zip(*promises).then do
|
108
|
+
handle_response(handler, req_parsed)
|
109
|
+
end.value!
|
110
|
+
rescue StandardError => e
|
111
|
+
handler_error(client, e)
|
112
|
+
end
|
113
|
+
|
114
|
+
def handle_response(handler, request)
|
115
|
+
if handler
|
116
|
+
if !request.client.closed?
|
117
|
+
handler.call(request)
|
118
|
+
else
|
119
|
+
Response.send_response(request.client, "Socket Closed", status: HttpStatusCodes::INTERNAL_SERVER_ERROR)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
handle_unknown_request(request.client)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def handler_error(client, error)
|
127
|
+
@logger.log("Error handling request: #{error.message}", LogMode::ERROR)
|
128
|
+
Response.send_response(client, "Internal Server Error", status: HttpStatusCodes::INTERNAL_SERVER_ERROR) if !client.closed?
|
129
|
+
end
|
130
|
+
|
131
|
+
def read_headers(request)
|
132
|
+
headers = {}
|
133
|
+
|
134
|
+
request.lines[1..-1].each do |line|
|
135
|
+
return headers if line == "\r\n"
|
136
|
+
|
137
|
+
header, value = line.split
|
138
|
+
header = normalize(header)
|
139
|
+
|
140
|
+
headers[header] = value
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def handle_unknown_request(client)
|
145
|
+
Response.send_response(client, @not_found_message, status: HttpStatusCodes::NOT_FOUND)
|
146
|
+
end
|
147
|
+
|
148
|
+
def normalize(header)
|
149
|
+
String(header).gsub(":", "").downcase.to_sym
|
150
|
+
end
|
151
|
+
|
152
|
+
def read_body(client, content_length)
|
153
|
+
content_length < 0 ? client.readpartial(READ_CHUNK) : ''
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/nonblock'
|
4
|
+
require 'stringio'
|
5
|
+
require 'fcntl'
|
6
|
+
|
7
|
+
module HttpStatusCodes
|
8
|
+
OK = 200
|
9
|
+
NOT_FOUND = 404
|
10
|
+
INTERNAL_SERVER_ERROR = 500
|
11
|
+
BAD_REQUEST = 400
|
12
|
+
end
|
13
|
+
|
14
|
+
class SocketInterceptor
|
15
|
+
attr_reader :buffer
|
16
|
+
|
17
|
+
def initialize(socket)
|
18
|
+
@socket = socket
|
19
|
+
@buffer = ""
|
20
|
+
end
|
21
|
+
|
22
|
+
def puts(data)
|
23
|
+
@buffer += data + "\n"
|
24
|
+
@socket.puts(data)
|
25
|
+
end
|
26
|
+
|
27
|
+
def flush
|
28
|
+
@socket.flush
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Response
|
33
|
+
class << self
|
34
|
+
attr_accessor :cors_headers
|
35
|
+
end
|
36
|
+
|
37
|
+
self.cors_headers = {}
|
38
|
+
|
39
|
+
def self.send_response(client = nil, body = nil, version: '1.1', status: HttpStatusCodes::OK, content_type: ContentTypes::HTML, custom_headers: {})
|
40
|
+
return if client.closed?
|
41
|
+
|
42
|
+
begin
|
43
|
+
if (client.nil? && body.nil?)
|
44
|
+
raise StandardError, "Client and body not set"
|
45
|
+
end
|
46
|
+
|
47
|
+
merged_hash = custom_headers.merge(self.cors_headers)
|
48
|
+
|
49
|
+
set_headers(client, version, status, content_type, merged_hash)
|
50
|
+
set_body(client, body)
|
51
|
+
client.flush
|
52
|
+
|
53
|
+
# intercepted_data = {
|
54
|
+
# version: version,
|
55
|
+
# status: status,
|
56
|
+
# content_type: content_type,
|
57
|
+
# headers: merged_hash,
|
58
|
+
# body: body,
|
59
|
+
# }
|
60
|
+
|
61
|
+
# puts intercepted_data
|
62
|
+
rescue Errno::EPIPE => e
|
63
|
+
puts "Error in send_response #{e.message}"
|
64
|
+
ensure
|
65
|
+
client.close unless client.closed?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.parse_request(client)
|
70
|
+
p client.recv(20)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.set_headers(client, version, status, content_type, custom_headers)
|
74
|
+
client.puts "HTTP/#{version} #{status}"
|
75
|
+
client.puts "Content-Type: #{content_type}"
|
76
|
+
|
77
|
+
custom_headers.each do |key, value|
|
78
|
+
client.puts "#{key}: #{value}"
|
79
|
+
end
|
80
|
+
|
81
|
+
client.puts
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.set_body(client, body = nil)
|
85
|
+
return if body.nil?
|
86
|
+
|
87
|
+
begin
|
88
|
+
client.puts body
|
89
|
+
rescue IO::WaitReadable
|
90
|
+
IO.select([client])
|
91
|
+
retry
|
92
|
+
rescue EOFError, Errno::ECONNRESET
|
93
|
+
puts "Client Disconnected"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Middlewares
|
99
|
+
class << self
|
100
|
+
attr_accessor :catcher, :cors, :default_cors_headers, :Middleware_Parser
|
101
|
+
end
|
102
|
+
|
103
|
+
self.Middleware_Parser = Struct.new(:middleware, :custom_headers)
|
104
|
+
|
105
|
+
self.catcher = lambda do |_, method, path, logger|
|
106
|
+
logger.log("[#{method}] - #{path}", LogMode::INFO)
|
107
|
+
end
|
108
|
+
|
109
|
+
self.default_cors_headers = {
|
110
|
+
'Access-Control-Allow-Origin' => '*',
|
111
|
+
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
112
|
+
'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
|
113
|
+
}
|
114
|
+
|
115
|
+
self.cors = lambda do |request, custom_cors_headers = {}|
|
116
|
+
begin
|
117
|
+
merged_headers = self.default_cors_headers.merge(custom_cors_headers)
|
118
|
+
|
119
|
+
merged_headers = Hash[merged_headers.to_a.uniq]
|
120
|
+
|
121
|
+
merged_headers.each do |key, value|
|
122
|
+
Response.cors_headers[key] = value
|
123
|
+
end
|
124
|
+
|
125
|
+
return if request.client.closed?
|
126
|
+
request.client.flush
|
127
|
+
rescue StandardError => e
|
128
|
+
p "Error on cors Middleware: #{e.message}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
module RequestMethods
|
134
|
+
GET = 'GET'
|
135
|
+
POST = 'POST'
|
136
|
+
DELETE = 'DELETE'
|
137
|
+
PUT = 'PUT'
|
138
|
+
OPTIONS = 'OPTIONS'
|
139
|
+
end
|
140
|
+
|
141
|
+
module ContentTypes
|
142
|
+
HTML = 'text/html'
|
143
|
+
JSON = 'application/json'
|
144
|
+
MJS = 'text/javascript'
|
145
|
+
MP3 = 'audio/mpeg'
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
module LogMode
|
150
|
+
DEBUG = 'DEBUG'
|
151
|
+
INFO = 'INFO'
|
152
|
+
WARN = 'WARN'
|
153
|
+
ERROR = 'ERROR'
|
154
|
+
FATAL = 'FATAL'
|
155
|
+
|
156
|
+
SEVERITIES = {
|
157
|
+
'DEBUG' => 0,
|
158
|
+
'INFO' => 1,
|
159
|
+
'WARN' => 2,
|
160
|
+
'ERROR' => 3,
|
161
|
+
'FATAL' => 4
|
162
|
+
}.freeze
|
163
|
+
end
|
164
|
+
|
165
|
+
class PicklesHttpServer
|
166
|
+
module Utils
|
167
|
+
include HttpStatusCodes
|
168
|
+
include ContentTypes
|
169
|
+
include LogMode
|
170
|
+
include RequestMethods
|
171
|
+
|
172
|
+
Request = Struct.new(:client, :body, :headers)
|
173
|
+
|
174
|
+
def self.parse_request(client, body, headers)
|
175
|
+
Request.new(client, body, headers)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/pickles_http.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pickles_http/server'
|
2
|
+
|
3
|
+
class PicklesHttpServer
|
4
|
+
def initialize(port: 8080, log_file: true, host: '127.0.0.1')
|
5
|
+
@socket = PicklesHttpServer::Server.new(port, log_file, host: host.to_s)
|
6
|
+
end
|
7
|
+
|
8
|
+
def start
|
9
|
+
begin
|
10
|
+
@socket.start()
|
11
|
+
rescue Interrupt
|
12
|
+
puts 'PicklesServer stopped by user, Bye 👋'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Method to add new route to PiclesServer
|
17
|
+
def add_route(method, path, handler)
|
18
|
+
@socket.add_route(method, path, handler)
|
19
|
+
end
|
20
|
+
|
21
|
+
def use(middleware, custom_cors_headers = {})
|
22
|
+
middleware_parsed = Middlewares.Middleware_Parser.new(middleware, custom_cors_headers)
|
23
|
+
@socket.use_middleware(middleware_parsed)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_server_options(option, value)
|
27
|
+
@socket.change_server_option(option, value)
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pickles_http
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Moisés Guerola
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple Http Framework gem
|
14
|
+
email: daw.moisesguerola@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/pickles_http.rb
|
20
|
+
- lib/pickles_http/logger.rb
|
21
|
+
- lib/pickles_http/router.rb
|
22
|
+
- lib/pickles_http/server.rb
|
23
|
+
- lib/pickles_http/utils.rb
|
24
|
+
homepage: https://rubygems.org/gems/pickles_http
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubygems_version: 3.5.6
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Simple HTTP Framework
|
47
|
+
test_files: []
|