Created
April 9, 2021 22:25
-
-
Save leandronsp/3dd724277a76363eb16e47568fb6af89 to your computer and use it in GitHub Desktop.
Yata HTTP Server in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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