Skip to content

Instantly share code, notes, and snippets.

@leandronsp
Created April 9, 2021 22:25
Show Gist options
  • Save leandronsp/3dd724277a76363eb16e47568fb6af89 to your computer and use it in GitHub Desktop.
Save leandronsp/3dd724277a76363eb16e47568fb6af89 to your computer and use it in GitHub Desktop.
Yata HTTP Server in Ruby
require 'cgi'
require 'uri'
require 'socket'
require './lib/routes'
class Application
def initialize(port)
@socket = TCPServer.new(port)
end
def self.serve!(port)
new(port).loop_forever!
end
def loop_forever!
loop do
# Waits for a new connection
client = @socket.accept
begin
process!(client)
rescue => error
puts error
puts error.backtrace
end
# Closes the client connection
client.close
end
end
private
def process!(client)
# Processes the request and routes to the application controllers
controller_response = Routes.route(*parse_request(client))
# Sends the response to the client
response = build_response(controller_response)
client.puts(response)
end
def parse_request(client)
verb, path, uri_params = process_firstline(client)
headers = process_headers(client)
cookie = process_cookie(headers['Cookie'])
body_params = process_body(client, headers['Content-Length'])
params = uri_params.merge(body_params)
[verb, path, params, headers, cookie]
end
def process_body(client, content_length)
return {} unless content_length
raw_body = client.read(content_length.to_i)
extract_params(CGI.unescape(raw_body))
end
def process_headers(client)
headers = {}
while line = client.gets
break if line == "\r\n"
header_name, header_value = line.split(": ")
headers[header_name] = header_value.gsub("\r\n", '')
end
headers
end
def process_cookie(cookie_value)
cookie = {}
return cookie unless cookie_value
name, value = cookie_value.split('=')
cookie[name.to_sym] = value
cookie
end
def process_firstline(client)
line = client.gets
raise StandardError.new('Could not process the request') unless line
verb, path, _ = line.split
uri_path = URI.parse(path)
# [GET, /hello, {}]
return [verb, uri_path.path, {}] unless uri_path.query
uri_params = extract_params(uri_path.query)
# [GET, /hello, { param_one: 1 }]
[verb, uri_path.path, uri_params]
end
def extract_params(params_string)
raw_params = params_string.split('&')
raw_params.each_with_object({}) do |raw_param, acc|
key, value = raw_param.split('=')
acc[key.to_sym] = value
end
end
def build_response(attrs = {})
status = attrs[:status]
body = attrs[:body] || ''
headers = attrs[:headers] || {}
"HTTP/1.1 #{status}\r\n#{stringify_headers(headers)}\r\n\r\n#{body}"
end
def stringify_headers(headers)
headers
.map(&method(:stringify_header))
.join("\r\n")
end
def stringify_header(name, value)
"#{name}: #{value}"
end
end
require './lib/request'
require './lib/response'
require './app/errors/unauthorized_error'
Dir[File.join(File.expand_path('..', __dir__), 'app', 'controllers', '*_controller.rb')].each { |f| require f }
class Routes
ROUTES_TABLE = {
'GET /' => :get_homepage_route,
'GET /login' => :get_login_route,
'POST /login' => :post_login_route,
'GET /register' => :get_register_route,
'POST /register' => :post_register_route,
'POST /logout' => :post_logout_route,
'POST /tasks' => :post_tasks_route,
'DELETE /tasks/:id' => :delete_tasks_route
}.freeze
def self.route(verb, path, params, headers, cookie)
request = Request.new(verb, path, params, headers, cookie)
response = Response.new
new(request, response).process
end
def initialize(request, response)
@request = request
@response = response
end
def process
return static_asset_route if @request.static_asset?
route = first_lookup || second_lookup
return not_found_route unless route
begin
send(route)
rescue UnauthorizedError
redirect_to_login
end
end
private
def first_lookup
ROUTES_TABLE["#{@request.verb} #{@request.path}"]
end
def second_lookup
constraint = @request.path.match(/^\/(tasks)\/([\w\d ]+)$/)
return unless constraint
resource_name, resource_id = constraint.values_at(1, 2)
@request.add_param(:id, resource_id)
ROUTES_TABLE["#{@request.verb} /#{resource_name}/:id"]
end
def redirect_to_login
{ status: 301, headers: { 'Location' => "#{FULL_HOST}/login" }}
end
def static_asset_route
return not_found_route unless File.exists?(@request.static_asset_path)
body = File.read(@request.static_asset_path)
{ status: 200, body: body }
end
def not_found_route
{ status: 404, body: '<h1>Not Found</h1>', headers: { 'Content-Type' => 'text/html' }}
end
def get_homepage_route
controller = HomeController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.show
end
def get_login_route
controller = LoginController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.show
end
def post_login_route
controller = LoginController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.create
end
def post_logout_route
controller = LoginController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.destroy
end
def get_register_route
controller = RegisterController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.show
end
def post_register_route
controller = RegisterController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.create
end
def post_tasks_route
controller = TasksController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.create
end
def delete_tasks_route
controller = TasksController.new(params: @request.params,
headers: @request.headers,
cookie: @request.cookie)
controller.destroy
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment