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 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
@@ -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: []