From 5a3f081804a57237656cad3a8b86500c571dcf89 Mon Sep 17 00:00:00 2001 From: Superkooka Date: Tue, 6 Apr 2021 19:56:37 +0200 Subject: [PATCH] very dirty commit --- .gitignore | 1 + README.md | 85 ++++++++++++- Vagrantfile | 2 + config.json | 18 +++ main.js | 16 +++ src/http/handler/compter_handler.cr | 135 ++++++++++++++++++++ src/http/handler/fast_cgi_handler.cr | 126 +++++++++++++++++++ src/http/handler/handler.cr | 9 ++ src/http/handler/proxy_handler.cr | 15 +++ src/http/handler/static_handler.cr | 28 +++++ src/http/helper.cr | 3 + src/http/message.cr | 142 ++++++++++++++++++++++ src/http/request.cr | 96 +++------------ src/http/response.cr | 58 ++++++--- src/http/status.cr | 75 ++++++++++++ src/main.cr | 39 +++++- web/cgi/index.php | 4 + web/compteur/main.js | 10 ++ static/hellow.html => web/home/index.html | 5 +- 19 files changed, 759 insertions(+), 108 deletions(-) create mode 100644 .gitignore create mode 100644 config.json create mode 100644 main.js create mode 100644 src/http/handler/compter_handler.cr create mode 100644 src/http/handler/fast_cgi_handler.cr create mode 100644 src/http/handler/handler.cr create mode 100644 src/http/handler/proxy_handler.cr create mode 100644 src/http/handler/static_handler.cr create mode 100644 src/http/helper.cr create mode 100644 src/http/message.cr create mode 100644 src/http/status.cr create mode 100644 web/cgi/index.php create mode 100644 web/compteur/main.js rename static/hellow.html => web/home/index.html (67%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50e1322 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/*.log diff --git a/README.md b/README.md index a68bd12..08f204d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,92 @@ -vagrant up +# Web Crystal + +WebCrystal est un serveur HTTP réalisé à l'occasion du défi de [NaN](https://discord.gg/zcWp9sC). + +Ce projet m'a permis d'apprendre Crystal, le code présente donc pas mal de bad-practice, de chose mal géré... + +## Consigne + +> ### Implémentation d'un serveur HTTP +> +> Défi sur une semaine, à rendre au plus tard le lundi 5 avril à 6h à votre ambassadeur préféré. Ce défi est considéré plus difficile que d'ordinaire, vous avez une semaine pour le résoudre. +> +> #### Le serveur +> +> Le serveur tourne en HTTP/1.0 derrière le port 8080 et est compatible avec les principaux navigateurs. Le serveur comprend les requêtes HEAD, GET, POST, PUT et DELETE. Le serveur expose une application "compteur" à l'url http://compteur.notaname.fr/, les autres url doivent renvoyer une page d'erreur au format HTML avec un code 404. Le serveur doit être capable de répondre à plusieurs utilisateurs à la fois. Le serveur peut être accessible à l'extérieur de votre réseau domestique. Le serveur peut comprendre TLS, dans quel cas il écoute sur le port 8443. +> +> Parce que le but de ce défi est d'implémenter un serveur web, il est bien entendu interdit d'utiliser un quelconque framework. Plus précisément, vous n'avez pas le droit d'utiliser une quelconque bibliothèque HTTP et vous vous contentez des bibliothèques réseaux et systèmes de votre langage. Le choix de votre stack système est libre, vous pouvez faire du multithreading, de l'asynchrone, etc. +> +> #### L'application +> +> L'application permet de manipuler des compteurs. Le serveur démarre avec deux compteurs : "carotte" et "etoile". Le compteur "carotte" est normal et est initialisé à 10. Un compteur "etoile" est le seul compteur spécial, il n'est pas possible de le modifier et il renvoie la somme de tous les autres compteurs. La valeur d'un compteur ne peut que croître. +>L'application compteur admet 5 routes (en plus de ``HEAD``): +> +> * ``GET /`` : retourne la liste de tous les compteurs. +> * ``GET /`` : retourne l'état du compteur . +> * ``POST /`` : crée un nouveau compteur et le renvoie. +> * ``PUT /`` : met à jour la valeur du compteur . +> * ``DELETE /`` : supprime le compteur . +> +> Il est possible de communiquer en ``x-www-form-urlencoded`` et en ``JSON`` avec l'application (requête). L'application est capable de répondre au format ``HTML``, au format ``JSON`` ou à défaut au format texte (réponse). Interessez vous aux headers ``Accept`` et ``Content-Type``. +> +> Pour simuler une application plus compliquée et mettre à mal votre implémentation réseau, chaque route simule des calculs et des requêtes à un service externe. Vous simulez les calculs en faisant tourner une boucle dans le vide à raison de 0.2 secondes. Vous simulez une requête externe en mettant votre processus en sleep pendant 0.3 secondes au milieu. +> +> Exemple en Python, après des tests vous avez trouvé qu'itérer sur 1 millard d'éléments prend 200 millisecondes. + +```python +counters = {'carotte': 10} + +def get(counter): + for i in range(1_000_000_000): pass + time.sleep(0.3) # remplacez par asyncio.sleep(0.3) si vous faites de l'async + + return b"{'name': 'carotte', 'value': %d}" % counters[counter] +``` +> #### Considérations techniques +> +> Pour faciliter l'implémentation, vous pouvez limiter la taille de la commande et des headers à maximum 1024 caractères par ligne. La RFC (de ce qu'on sait) ne précise rien à ce propos et la plupart des serveurs permettent 8k ou 16k caractères par ligne dans les headers. +> +> #### Ressources +> +> Voici une liste de ressource que nous vous conseillons de lire. Nous ajouterons à cette liste toutes les ressources que vous nous conseillerez. +> +>* https://fr.wikipedia.org/wiki/Hypertext_Transfer_Protocol +>* https://www.pierre-giraud.com/http-reseau-securite-cours/ +>* http://www.kegel.com/c10k.html +>* https://www.w3.org/Protocols/HTTP/1.0/spec.html + +## Lancer le serveur + +Pour lancer le projet simplement vous pouvez utiliser vagrant, ou alors installer crystal + +```vagrant up vagrant ssh cd /vagrant clear && crystal run src/main.cr +``` -vagrant halt +Pour simuler un test de charge +``` vegeta attack -targets targets.txt -rate=20 -duration=30s vegeta attack -targets=targets.txt -name=300qps -rate=300 -duration=25s > results.300qps.bin;cat results.300qps.bin | vegeta plot > plot.300qps.html +``` + +## Pour compiler le serveur + +``` +crystal build ./src/main.cr +mv ./src/main ./web +``` + +## TODO + +* Ajouter le logger +* Ecrire des test +* Ecrire une CI, capable de crée les releases... +* Utiliser un analyser static comme [ameba](https://github.com/crystal-ameba/ameba). +* CGI +* Configuration (json --> DSL) +* TLS diff --git a/Vagrantfile b/Vagrantfile index f8ce974..2056397 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -7,6 +7,8 @@ Vagrant.configure("2") do |config| s.inline = <<-SHELL curl -fsSL https://crystal-lang.org/install.sh | sudo bash + sudo apt install php-fpm -y + curl -LO https://github.com/tsenart/vegeta/releases/download/v12.7.0/vegeta-12.7.0-linux-amd64.tar.gz tar -zxvf vegeta-12.7.0-linux-amd64.tar.gz sudo mv ./vegeta /usr/bin/vegeta diff --git a/config.json b/config.json new file mode 100644 index 0000000..291bcb3 --- /dev/null +++ b/config.json @@ -0,0 +1,18 @@ +{ + "notaname.home": { + "handler": "static_serve", + + "document_root": "/var/www/notaname/home" + }, + + "compteur.notaname.home": { + "handler": "internal.compter" + }, + + "compteur.cheat.notaname.home": { + "handler": "proxy_pass", + + "host": "127.0.0.1", + "port": 8080 + } +} diff --git a/main.js b/main.js new file mode 100644 index 0000000..903eb7c --- /dev/null +++ b/main.js @@ -0,0 +1,16 @@ +var http = require("http"); + +var serv = http.createServer( + + function (req, res) { + console.log("ping !"); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("Hello world !"); + + res.end(); + } +); + +serv.listen(8080); + +console.log("Server running at http://localhost:8080"); diff --git a/src/http/handler/compter_handler.cr b/src/http/handler/compter_handler.cr new file mode 100644 index 0000000..ad8dc4b --- /dev/null +++ b/src/http/handler/compter_handler.cr @@ -0,0 +1,135 @@ +require "../method" + +class CompterHandler < Handler + @@compters : Hash(String, Int32) = {"carotte" => 40, "etoile" => 40} + + def handle(request : Request) : Response + if request.method == Method::GET && !(regexified_uri = /^\/$/.match(request.uri)).nil? + return show_all + elsif request.method == Method::POST && !(regexified_uri = /^\/$/.match(request.uri)).nil? + if request.is_form_body || request.is_json_body + body = request.get_body + else + return Response.new "HTTP", "1.0", HTTPStatus::BAD_REQUEST + end + + return create body + elsif request.method == Method::GET && !(regexified_uri = /^\/(?'compter_name'[a-zA-Z0-9]+)$/.match(request.uri)).nil? + return show regexified_uri["compter_name"] + elsif request.method == Method::PUT && !(regexified_uri = /^\/(?'compter_name'[a-zA-Z0-9]+)$/.match(request.uri)).nil? + if request.is_form_body || request.is_json_body + body = request.get_body + else + return Response.new "HTTP", "1.0", HTTPStatus::BAD_REQUEST + end + + return increment regexified_uri["compter_name"], body + elsif request.method == Method::DELETE && !(regexified_uri = /^\/(?'compter_name'[a-zA-Z0-9]+)$/.match(request.uri)).nil? + return delete regexified_uri["compter_name"] + end + + Response.new "HTTP", "1.0", HTTPStatus::NOT_FOUND + end + + def show_all : Response + response = Response.new "HTTP", "1.0", HTTPStatus::OK + response.body = @@compters.to_json + + response + end + + def show(compter : String) + if !@@compters[compter]? + return Response.new "HTTP", "1.0", HTTPStatus::NOT_FOUND + end + + response = Response.new "HTTP", "1.0", HTTPStatus::OK + response.body = "{\"" + compter + "\":" + "\"" + @@compters[compter].to_s + "\"}" + + response + end + + def create(body) + if body.nil? + raise "WTF" + end + + compter_name = body["name"]? + + # Broken, I dont know why ? + + # if !compter_name || !compter_name.is_a?(String) + # return Response.new "HTTP", "1.0", HTTPStatus::BAD_REQUEST + # end + + if @@compters[compter_name.to_s]? + return Response.new "HTTP", "1.0", HTTPStatus::CONFLICT + end + + compter_value = body["value"]? + if !compter_value + compter_value = 0 + end + + # Can be broken with json body + @@compters[compter_name.to_s] = compter_value.to_s.to_i + + calcul_etoile + show compter_name.to_s + end + + def increment(compter : String, body) + if !@@compters[compter]? + return Response.new "HTTP", "1.0", HTTPStatus::NOT_FOUND + end + + if body.nil? + raise "WTF" + end + + compter_value = body["value"]? + + if !compter_value || !compter_value.is_a?(String) + return Response.new "HTTP", "1.0", HTTPStatus::BAD_REQUEST + end + + if compter == "etoile" + return Response.new "HTTP", "1.0", HTTPStatus::FORBIDDEN + end + + if @@compters[compter] > compter_value.to_i + return Response.new "HTTP", "1.0", HTTPStatus::NOT_ACCEPTABLE + end + + @@compters[compter] = compter_value.to_i + + calcul_etoile + show compter + end + + def delete(compter : String) + if @@compters[compter]?.nil? + return Response.new "HTTP", "1.0", HTTPStatus::NOT_FOUND + end + + if compter == "etoile" + return Response.new "HTTP", "1.0", HTTPStatus::FORBIDDEN + end + + @@compters.delete(compter) + calcul_etoile + Response.new "HTTP", "1.0", HTTPStatus::NO_CONTENT + end + + def calcul_etoile() + @@compters["etoile"] = 0 + + @@compters.each do |compter| + if compter[0] == "etoile" + next + end + + @@compters["etoile"] += @@compters[compter[0]] + end + end +end diff --git a/src/http/handler/fast_cgi_handler.cr b/src/http/handler/fast_cgi_handler.cr new file mode 100644 index 0000000..c88ad63 --- /dev/null +++ b/src/http/handler/fast_cgi_handler.cr @@ -0,0 +1,126 @@ +require "./handler" + +# Translation of https://github.com/adoy/PHP-FastCGI-Client/blob/master/src/Adoy/FastCGI/Client.php in crystal +# Handles communication with a FastCGI application + +FAST_CGI_VERSION_1 = 1 + +FAST_CGI_BEGIN_REQUEST = 1 +FAST_CGI_ABORT_REQUEST = 2 +FAST_CGI_END_REQUEST = 3 +FAST_CGI_PARAMS = 4 +FAST_CGI_STDIN = 5 +FAST_CGI_STDOUT = 6 +FAST_CGI_STDERR = 7 +FAST_CGI_DATA = 8 +FAST_CGI_GET_VALUES = 9 +FAST_CGI_GET_VALUES_RESULT = 10 +FAST_CGI_UNKNOWN_TYPE = 11 +FAST_CGI_MAXTYPE = UNKNOWN_TYPE + +FAST_CGI_RESPONDER = 1 +FAST_CGI_AUTHORIZER = 2 +FAST_CGI_FILTER = 3 + +FAST_CGI_REQUEST_COMPLETE = 0 +FAST_CGI_CANT_MPX_CONN = 1 +FAST_CGI_OVERLOADED = 2 +FAST_CGI_UNKNOWN_ROLE = 3 + +FAST_CGI_MAX_CONNS = "MAX_CONNS" +FAST_CGI_MAX_REQS = "MAX_REQS" +FAST_CGI_MPXS_CONNS = "MPXS_CONNS" + +FAST_CGI_HEADER_LEN = 8 + +FAST_CGI_MAX_LENGTH = 0xffff + +class FastCGIHandler < Handler + # Broken handler + id = rand((1 << 16) - 1) + keep_alive = 1 + fast_cgi_request = build_packet(FAST_CGI_BEGIN_REQUEST, "#{0.chr}#{FAST_CGI_RESPONDER.chr}#{keep_alive.chr}#{0.chr}#{0.chr}#{0.chr}#{0.chr}#{0.chr}", id) + + fast_cgi_request_environnement = "" + environnement = Hash(String, String).new + environnement["GATEWAY_INTERFACE"] = "FastCGI/1.0" + environnement["DOCUMENT_ROOT"] = "/vagrant" + environnement["DOCUMENT_URI"] = "/vagrant/index.php" + environnement["PATH_INFO"] = "/vagrant/index.php" + environnement["REQUEST_URI"] = "/index.php" + environnement["REQUEST_METHOD"] = "GET" + environnement["SCRIPT_FILENAME"] = "index.php" + environnement["SERVER_SOFTWARE"] = "php/fcgiclient" + environnement["REMOTE_ADDR"] = "127.0.0.1" + environnement["REMOTE_PORT"] = "9985" + environnement["SERVER_ADDR"] = "127.0.0.1" + environnement["SERVER_PORT"] = "8080" + environnement["SERVER_NAME"] = "mag-tured" + environnement["SERVER_PROTOCOL"] = "HTTP/1.0" + + environnement.each do |key, value| + fast_cgi_request_environnement += build_key_value_pair(key, value, id) + end + + if !fast_cgi_request_environnement.empty? + fast_cgi_request += build_packet(FAST_CGI_PARAMS, fast_cgi_request_environnement, id) + end + + fast_cgi_request += build_packet(FAST_CGI_PARAMS, "", id) + + if !request.body.empty? + stdin = request.body + + until stdin.bytesize < FAST_CGI_MAX_LENGTH + chunk = stdin[0, FAST_CGI_MAX_LENGTH] + fast_cgi_request += build_packet(FAST_CGI_STDIN, chunk, id) + stdin = stdin.byte_slice(0, FAST_CGI_MAX_LENGTH) + end + + fast_cgi_request += build_packet(FAST_CGI_STDIN, stdin, id) + end + + fast_cgi_request += build_packet(FAST_CGI_STDIN, "", id) + + UNIXSocket.open("/run/php/php7.2-fpm.sock") do |fpm_socket| + fpm_socket << fast_cgi_request + + message = fpm_socket.gets + + end + + def build_packet(type : Int32, content : String, id : Int32) + content_length = content.bytesize + + return String.build do |io| + io << FAST_CGI_VERSION_1.chr + io << type.chr + io << ((id >> 8) & 0xFF).chr + io << (id & 0xFF).chr + io << ((content_length >> 8) & 0xFF).chr + io << (content_length & 0xFF).chr + io << 0.chr + io << 0.chr + io << content + end + end + + def build_key_value_pair(key : String, value : String, id : Int32) + key_lenght = key.bytesize + value_lenght = value.bytesize + + if 128 <= key_lenght + pair = "#{key_lenght.chr}" + else + pair = "#{((key_lenght >> 24) | 0x80).chr}#{((key_lenght >> 16) & 0xFF).chr}#{((key_lenght >> 8) & 0xFF)}#{(key_lenght & 0xFF).chr}" + end + + if 128 <= value_lenght + pair = "#{value_lenght.chr}" + else + pair = "#{((value_lenght >> 24) | 0x80).chr}#{((value_lenght >> 16) & 0xFF).chr}#{((value_lenght >> 8) & 0xFF)}#{(value_lenght & 0xFF).chr}" + end + + return "#{pair}#{key}#{value}" + end +end diff --git a/src/http/handler/handler.cr b/src/http/handler/handler.cr new file mode 100644 index 0000000..f267b56 --- /dev/null +++ b/src/http/handler/handler.cr @@ -0,0 +1,9 @@ +require "json" +require "../request" + +abstract class Handler + def initialize(@configuration : JSON::Any) + end + + abstract def handle(request : Request) : Response +end diff --git a/src/http/handler/proxy_handler.cr b/src/http/handler/proxy_handler.cr new file mode 100644 index 0000000..8f1e4d5 --- /dev/null +++ b/src/http/handler/proxy_handler.cr @@ -0,0 +1,15 @@ +require "./handler" +require "../response" + +class ProxyHandler < Handler + def handle(request : Request) : Response + puts "ping" + host = @configuration["host"].to_s + port = @configuration["port"].to_s.to_i + + return TCPSocket.open host, port do |tcp_socket| + tcp_socket << request.to_s + return Response.new tcp_socket.receive[0] + end + end +end diff --git a/src/http/handler/static_handler.cr b/src/http/handler/static_handler.cr new file mode 100644 index 0000000..87a88e8 --- /dev/null +++ b/src/http/handler/static_handler.cr @@ -0,0 +1,28 @@ +require "./handler" + +class StaticHandler < Handler + def handle(request : Request) : Response + file = @configuration["document_root"].to_s + request.uri + + if request.uri.ends_with? "/" + file = @configuration["document_root"].to_s + request.uri + "index.html" + end + + if !File.exists?(file) + response = Response.new "HTTP", "1.0", HTTPStatus::NOT_FOUND + response.body = "404" + return response + end + + if !File.info(file).permissions.owner_read? + response = Response.new "HTTP", "1.0", HTTPStatus::UNAUTHORIZED + response.body = "403" + return response + end + + response = Response.new "HTTP", "1.0", HTTPStatus::OK + response.body = File.read(file) + + return response + end +end diff --git a/src/http/helper.cr b/src/http/helper.cr new file mode 100644 index 0000000..f6a3aec --- /dev/null +++ b/src/http/helper.cr @@ -0,0 +1,3 @@ +class Helper + +end \ No newline at end of file diff --git a/src/http/message.cr b/src/http/message.cr new file mode 100644 index 0000000..af02e3e --- /dev/null +++ b/src/http/message.cr @@ -0,0 +1,142 @@ +require "./message" +require "./method" +require "uri" +require "json" + +class Message + + @@status_line_regex = /(?'method'[A-Z]+) (?'uri'[^ ]+) (?'protocol'(?'protocol_name'[^ ]+)\/(?'protocol_version'[0-9]+.[0-9]+))/ + @@header_line_regex = /^(?'name'[^:]+): (?'values'.+)/ + + @protocol_name = "HTTP" + @protocol_version = "1.0" + @method : Method = Method::NON_STANDARD + @uri : String = "/" + + @headers : Hash(String, String) = {} of String => String + @body : String = "" + + def parse_message(request : String) + protocol_name = Nil + protocol_version = Nil + method = Method::NON_STANDARD + uri = "/" + headers = {} of String => String + body = "" + + current_header = "" + is_body = false + is_status_line = true + + request.chars.each do |char| + if is_body + body += char + next + end + + current_header += char + + if current_header == "\r\n" + is_body = true + next + end + + if current_header.ends_with?("\r\n") # End of header + if !is_status_line + regexified_header = @@header_line_regex.match(current_header) + if regexified_header.nil? + raise "Header regex is broken :aie:" + end + + headers[regexified_header["name"]] = regexified_header["values"].gsub({"\r": "", "\n": ""}) + else + is_status_line = false + regexified_status_line = @@status_line_regex.match(current_header) + if regexified_status_line.nil? + raise "Status line regex is broken :aie:" + end + + method = Method.parse?(regexified_status_line["method"]) + uri = regexified_status_line["uri"] + protocol_name = regexified_status_line["protocol_name"] + protocol_version = regexified_status_line["protocol_version"] + end + current_header = "" + end + end + + return { + "protocol_name" => protocol_name, + "protocol_version" => protocol_version, + "method" => method, + "uri" => uri, + "headers" => headers, + "body" => body + } + end + + def parse_form_body(body : String) + form_body = Hash(String, String).new() + + key = "" + value = "" + is_key = true + body.chars.each do |char| + if char == '=' + is_key = false + next + end + + if char == '&' + form_body[key] = value[0, value.size - 1] + key = "" + value = "" + next + end + + if is_key + key += char + else + value += char + end + end + form_body[key] = value + return form_body + end + + def is_form_body + return "application/x-www-form-urlencoded" == @headers["Content-Type"]? + end + + def is_json_body + return "application/json" == @headers["Content-Type"]? + end + + def get_body + if is_json_body + return Hash(String, JSON::Any).from_json @body + elsif is_form_body + return parse_form_body @body + end + end + + def header(header : String) + return @headers[header]? + end + + def headers + return @headers + end + + def body : String + return @body + end + + def method : Method + return @method + end + + def uri: String + return @uri + end +end diff --git a/src/http/request.cr b/src/http/request.cr index 4f57d73..91c7971 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -1,99 +1,31 @@ +require "./message" require "./method" require "uri" +require "json" -class Request +class Request < Message - @method : Method = Method::NON_STANDARD - @protocol_name : String | Nil - @protocol_version : String | Nil - @headers : Hash(String, String) = Hash(String, String).new() - @body : String = "" - - @@status_line_regex = /(?'method'[A-Z]+) (?'uri'[^ ]+) (?'protocol'(?'protocol_name'[^ ]+)\/(?'protocol_version'[0-9]+.[0-9]+))/ - @@header_line_regex = /^(?'name'[^:]+): (?'values'.+)/ + @plain_request : String = "" def initialize(socket) if socket.nil? raise "Oooops" end - plain_request = socket.receive[0] + @plain_request = socket.receive[0] + req = parse_message(@plain_request) - current_header = "" - is_body = false - is_status_line = true + @protocol_name = req["protocol_name"].as(String) + @protocol_version = req["protocol_version"].as(String) + @method = req["method"].as(Method) + @uri = req["uri"].as(String) - plain_request.chars.each do |char| - if is_body - @body += char - next - end + @headers = req["headers"].as(Hash(String, String)) + @body = req["body"].as(String) - current_header += char - - if current_header == "\r\n" - is_body = true - next - end - - if current_header.ends_with?("\r\n") # End of header - if !is_status_line - regexified_header = @@header_line_regex.match(current_header) - if regexified_header.nil? - raise "Header regex is broken :aie:" - end - - @headers[regexified_header["name"]] = regexified_header["values"].gsub({"\r": "", "\n": ""}) - else - is_status_line = false - regexified_status_line = @@status_line_regex.match(current_header) - if regexified_status_line.nil? - raise "Status line regex is broken :aie:" - end - - method = Method.parse?(regexified_status_line["method"]) - if method.nil? - @method = Method::NON_STANDARD - else - @method = method - end - - protocol_name = regexified_status_line["protocol_name"] - if protocol_name.nil? - raise "Status Line must have a valid protocol name" - else - @protocol_name = protocol_name - end - - protocol_version = regexified_status_line["protocol_version"] - if protocol_version.nil? - raise "Status Line must have a valid protocol version" - else - @protocol_version = protocol_version - end - end - current_header = "" - end - end - - if @protocol_name.nil? - raise "Request must have a protocol name in status line" - end - - if @protocol_version.nil? - raise "Request must have a protocol version in status line" - end - - puts "Method: " + @method.to_s - puts "Protocol name: " + @protocol_name.to_s - puts "Protocol version: " + @protocol_version.to_s - puts "Headers: " + @headers.to_s - puts "Body: " - puts @body - puts "\r\n" end - def method : Method - return @method + def to_s : String + return @plain_request end end diff --git a/src/http/response.cr b/src/http/response.cr index b66f143..cfe519f 100644 --- a/src/http/response.cr +++ b/src/http/response.cr @@ -1,21 +1,49 @@ -class Response - def initialize() - @headers = [ - "HTTP/1.1 200 OK" - ] +require "./message" +require "./status" + +class Response < Message + + @status : HTTPStatus = HTTPStatus::OK + + def initialize (response : String) + req = parse_message(response) + + @protocol_name = req["protocol_name"].as(String) + @protocol_version = req["protocol_version"].as(String) + @method = req["method"].as(Method) + @uri = req["uri"].as(String) + + @headers = req["headers"].as(Hash(String, String)) + @body = req["body"].as(String) end - def to_s() - body = File.read("/vagrant/static/hellow.html") - @headers << "Content-Length: " + (body.bytesize + 1).to_s() # +1 for CRLF ? - - request = "" - @headers.each() do |header| - request += header + "\r\n" - end - request += "\r\n" + def initialize(protocol_name : String, protocol_version : String, status : HTTPStatus) + # TODO: Send true HTTP Status name + # @status_line = protocol_name + "/" + protocol_version + " " + status.value.to_s + " " + status.to_s + "\r\n" + end - request += body + def addHeader(name : String, value : String) + + end + + def body + @body = body + end + + def body=(@body : String) + end + + def to_s + @headers["Content-Length"] = @body.bytesize.to_s + + request = @protocol_name + "/" + @protocol_version + " " + @status.value.to_s + " " + @status.to_s + "\r\n" + + @headers.each do |name, value| + request += name + ": " + value + "\r\n" + end + + request += "\r\n" + request += @body return request end diff --git a/src/http/status.cr b/src/http/status.cr new file mode 100644 index 0000000..4cfa408 --- /dev/null +++ b/src/http/status.cr @@ -0,0 +1,75 @@ +enum HTTPStatus + # 1xx Informational + CONTINUE = 100 + SWITCHING_PROTOCOLS = 101 + PROCESSING = 102 + EARLY_HINT = 103 + + # 2xx Success + OK = 200 + CREATED = 201 + ACCEPTED = 202 + NON_AUTHORITATIVE_INFORMATION = 203 + NO_CONTENT = 204 + RESET_CONTENT = 205 + PARTIAL_CONTENT = 206 + MULTI_STATUS = 207 + ALREADY_REPORTED = 208 + IM_USED = 226 + + # 3xx Redirection + MULTIPLE_CHOICES = 300 + MOVED_PERMANENTLY = 301 + FOUND = 302 + SEE_OTHER = 303 + NOT_MODIFIED = 304 + USE_PROXY = 305 + UNUSED = 306 + TEMPORARY_REDIRECT = 307 + PERMANENT_REDIRECT = 308 + + # 4xx Client Error + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + PAYEMENT_REQUIRED = 402 + FORBIDDEN = 403 + NOT_FOUND = 404 + METHOD_NOT_ALLOWED = 405 + NOT_ACCEPTABLE = 406 + PROXY_AUTHENTICATION_REQUIRED = 407 + REQUEST_TIMEOUT = 408 + CONFLICT = 409 + GONE = 410 + LENGTH_REQUIRED = 411 + PRECONDITION_FAILED = 412 + PAYLOAD_TOO_LARGE = 413 + URI_TOO_LONG = 414 + UNSUPPORTED_MEDIA_TYPE = 415 + RANGE_NOT_SATISFIABLE = 416 + EXPECTATION_FAILED = 417 + IM_A_TEAPOT = 418 + MISDIRECTED_REQUEST = 421 + UNPROCESSABLE_ENTITY = 422 + LOCKED = 423 + FAILED_DEPENDENCY = 424 + TOO_EARLY = 425 + UPGRADE_REQUIRED = 426 + PRECONDITION_REQUIRED = 428 + TOO_MANY_REQUESTS = 429 + REQUEST_HEADER_FILEDS_TOO_LARGE = 431 + UNAVAILABLE_FOR_LEGA_REASONS = 451 + + # 5xx Server Error + INTERNAL_SERVER_ERROR = 500 + NOT_IMPLEMENTED = 501 + BAD_GATEWAY = 502 + SERVICE_UNAVAILABLE = 503 + GATEWAY_TIMEOUT = 504 + HTTP_VERSION_NOT_SUPPORTED = 505 + VARIANT_ALSO_NEGOTIATES = 506 + INSUFFICIENT_STORIAGE = 507 + LOOP_DETECTED = 508 + BANDWIDTH_LIMIT_EXEEDED = 509 + NOT_EXTENDED = 510 + NETWORK_AUTHENTIFICATION_REQUIRED = 511 +end \ No newline at end of file diff --git a/src/main.cr b/src/main.cr index f6cc909..33e28cc 100644 --- a/src/main.cr +++ b/src/main.cr @@ -1,19 +1,46 @@ require "socket" require "option_parser" +require "env" +require "json" require "./http/request" require "./http/response" +require "./http/handler/static_handler" +require "./http/handler/proxy_handler" +require "./http/handler/compter_handler" + def handle_request(client) request = Request.new(client) - response = Response.new() + puts request + configuration_file = File.open("/home/kooka/config.json") do |file| + file.gets_to_end + end - client.puts response.to_s() - - client.close + configuration = Hash(String, JSON::Any).from_json configuration_file + configuration.each do |key, value| + if request.header("Host") != key + next + end + + case value["handler"] + when "proxy_pass" + handler = ProxyHandler.new value + when "static_serve" + handler = StaticHandler.new value + when "internal.compter" + handler = CompterHandler.new value + else + client << Response.new "HTTP", "1.0", HTTPStatus::BAD_GATEWAY + break + end + + client << handler.handle(request).to_s + break + end end -tcp_port = 8000 +tcp_port = 8090 hostname = "127.0.0.1" OptionParser.parse do |parser| @@ -29,7 +56,7 @@ OptionParser.parse do |parser| exit end parser.invalid_option do |flag| - STDERR.puts "ERROR: #{flag} is not a valid option." + STDERR.puts "ERROR: #{flag} is not a valid option." STDERR.puts parser exit(1) end diff --git a/web/cgi/index.php b/web/cgi/index.php new file mode 100644 index 0000000..a905b0c --- /dev/null +++ b/web/cgi/index.php @@ -0,0 +1,4 @@ + - Wesh + Document -

Hellow World !

-

Cimer Nuker pour la découverte 😉

+

Salut

\ No newline at end of file