HTTP
Hypertext Transfer Protocol: Transfer von Webseiten und Zugemüse (auch einfach Dateien).
Aufgabe
Programmieren Sie einen minimalen Webserver, der folgende Anforderungen erfüllt:
- Es gibt einen Ordner html, indem Dateien (und Unterordner) liegen, die der Webserver ausliefern kann.
- Prüfen Sie den angeforderten Pfad. Insbesondere darf es nicht möglich sein, auf Dateien ausserhalb des Verzeichnisses html zuzugreifen.
- Schreiben Sie eine minimale html-Datei, die auch ein Bild enthält und testen Sie ihren Webserver damit.
Hilfestellungen
HTTP Protokoll
Minimale HTTP-Anfrage (Achtung, verwendet “\r\n” für einen Zeilenumbruch (wie DOS), nicht nur einfach “\n” (wie Unix).
GET /bla.html HTTP/1.1\r\n Host: fginfo.ksbg.ch:42001\r\n Connection: close \r\n \r\n
Die erste Zeile enthält Methode (GET), Pfad /bla.html und Protokollversion. Die zweite Zeile enthält den Servernamen und evtl. den Port Die dritte Zeile ist optional Zwei leere Zeilen schliessen die Anfrage ab.
Die Minimale Antwort sieht wie folgt aus:
HTTP/1.1 200 OK\r\n Content-Type: text/html\r\n Content-Length: 1354\r\n \r\n .... DATEN ....
Oder natürlich
HTTP/1.1 404 Not Found
Als Content-Type kommt z.B. auch image/png oder imag/jpg in Frage.
Ruby File Access
Siehe auch https://ruby-doc.org/core-2.2.0/File.html
- File.exists?(pfad) Überprüft, ob die Datei existiert.
- File.read(pfad) Liefert den Inhalt einer Datei als String zurück
- File.absolute_path(pfad) Liefert den absoluten Pfad zurück (z.B. /home/hansli/server/html/index.html)
Um mit Verzeichnissen zu arbeiten, siehe auch https://ruby-doc.org/core-2.2.0/File.html
- Dir.pwd liefert den aktuellen Pfad
- Dir.glob(“html/*”) liefert ein Array mit allen Dateien im Verzeichnis html
Lösungsvorschlag
- webserver.rb
# coding: utf-8 require 'socket' # Damit Ruby auch bei Fehlern im Thread abbricht mit Fehlermeldung Thread.abort_on_exception=true # 2 Methoden für Fehlermeldungen def notfound(client, pfad, debug="") msg = "<!DOCTYPE html><html><body><h1>404 Not found</h1>Sorry, ich bin halt nur ein Webserver... #{pfad} habe ich nicht gefunden...<br><br><h2>Debug Info</h2><pre>#{debug}</pre></body></html>" client.puts "HTTP/1.1 404 Not Found\r Content-Type: text/html\r Content-Length: #{msg.size}\r\n\r\n" client.puts msg puts "Got a 404" end def badRequest(client, req, debug="") msg = "<!DOCTYPE html><html><body><h1>400 Bad request</h1>Sorry, die Anfrage #{req} verstehe ich nicht.<br><br><h2>Debug Info</h2><pre>#{debug}</pre></body></html>" client.puts "HTTP/1.1 400 Bad request\r Content-Type: text/html\r Content-Length: #{msg.size}\r\n\r\n" client.puts msg puts "Got a 400" end # Methode für Dirlisting def dirlist(client,basepfad, abspfad, relpfad) relpfad = "/" if (relpfad=="") puts "Dirlist für #{abspfad}, #{relpfad}" files = Dir.glob(abspfad+"/*") # Alle Dateien im Verzeichnis relpfad.gsub(/\/+$/,"") # Remove trailing slash msg = "<!DOCTYPE html><html><body><h1>Directory #{relpfad}</h1><ul>" relpfad.gsub!(/\/+$/,"") # Remove trailing slashes files.each{|f| f = File.basename(f) puts relpfad puts f msg+="<li><a href=\"#{relpfad + "/" +f}\">#{f}</a></li>\n" } client.puts "HTTP/1.1 200 OK\r Content-Type: text/html\r Content-Length: #{msg.size}\r\n\r\n" client.puts msg puts "Listed #{abspfad}" end # Server auf port 420XX laufen lassen server = TCPServer.open(42001) # Endlos-Schleife loop { # Auf Verbindung warten. # Wenn verbunden, einen Thread damit beschäftigen # und gleich auf nächste Verbindung warten Thread.start(server.accept) { |client| # Request einlesen, zeilenweise im Array all speichern all = [] while ((req=client.gets.chomp)!="") puts req all.push(req) end p all # Erste Zeile analysieren pfad = all[0].scan(/^GET ([^ ]+)/) # Wenn kein Match, also bad request (nicht http-protokoll) p pfad if (pfad.size==0) puts "400!" badRequest(client, all[0], all.join("\n")) else puts # Dem Request den ordner 'html' vorne anfügen pfad = "html"+pfad[0][0] # Daraus den absoluten Pfad erstellen pfad = File.absolute_path(pfad) # Überprüfen, ob auch im richtigen verzeichnis # Aktueller Pfad plus html richtig = File.absolute_path(Dir.pwd+"/html") p pfad p richtig if pfad.start_with?(richtig) && File.exists?(pfad) if (File.directory?(pfad)) dirlist(client,richtig, pfad, pfad.gsub(richtig,"")) else puts "Got a 200" # Typ bestimmen: Paare aus RegEx und MIME-Type (/i macht die regex case-insensitive) types = [[/html?$/i,"text/html"], [/jpe?g$/i,'image/jpg'], [/png$/i,'image/png'], [/gif/i, 'image/gif'], [/txt/i, 'text/plain']] type = "text/html" # Default, wenn nichts anderes gefunden types.each{|t| if pfad =~ t[0] type=t[1] end } daten = File.read(pfad) client.puts "HTTP/1.1 200 OK\r Content-Type: #{type}\r Content-Length: #{daten.size}\r\n\r\n" client.print daten end else puts "Not found" notfound(client, pfad, all.join("\n")) end end client.close } }