very dirty commit
This commit is contained in:
parent
ed90eb566e
commit
5a3f081804
|
|
@ -0,0 +1 @@
|
|||
/*.log
|
||||
85
README.md
85
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 /<compteur>`` : retourne l'état du compteur <compteur>.
|
||||
> * ``POST /`` : crée un nouveau compteur et le renvoie.
|
||||
> * ``PUT /<compteur>`` : met à jour la valeur du compteur <compteur>.
|
||||
> * ``DELETE /<compteur>`` : supprime le compteur <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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
require "json"
|
||||
require "../request"
|
||||
|
||||
abstract class Handler
|
||||
def initialize(@configuration : JSON::Any)
|
||||
end
|
||||
|
||||
abstract def handle(request : Request) : Response
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
class Helper
|
||||
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
@headers = req["headers"].as(Hash(String, String))
|
||||
@body = req["body"].as(String)
|
||||
|
||||
plain_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"])
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
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
|
||||
|
||||
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
|
||||
request += @body
|
||||
|
||||
return request
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
35
src/main.cr
35
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()
|
||||
configuration = Hash(String, JSON::Any).from_json configuration_file
|
||||
configuration.each do |key, value|
|
||||
if request.header("Host") != key
|
||||
next
|
||||
end
|
||||
|
||||
client.close
|
||||
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|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
file_get_contents('http://127.0.0.1:8080');
|
||||
$file = fopen('/vagrant/boomer.txt', 'w') or die("PHP t'es naze");
|
||||
var_dump(getenv());
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
const http = require('http');
|
||||
|
||||
const requestListener = function (req, res) {
|
||||
console.log(req.method.toUpperCase())
|
||||
res.writeHead(200);
|
||||
res.end('Hello, World!');
|
||||
}
|
||||
|
||||
const server = http.createServer(requestListener);
|
||||
server.listen(8080);
|
||||
|
|
@ -4,10 +4,9 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wesh</title>
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hellow World !</h1>
|
||||
<p>Cimer Nuker pour la découverte 😉</p>
|
||||
<p>Salut</p>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue