Skip to content

Instantly share code, notes, and snippets.

@menzerath
Last active March 25, 2017 14:52
Show Gist options
  • Save menzerath/16dbf192de45e11eab48 to your computer and use it in GitHub Desktop.
Save menzerath/16dbf192de45e11eab48 to your computer and use it in GitHub Desktop.
Facharbeit: Funktionsweise des HTTP-Protokolls und Implementation eines einfachen Webservers in Java - http://menzerath.eu/artikel/wie-funktioniert-das-http-protokoll/
package de.menzerath.util;
import java.io.File;
import java.text.DecimalFormat;
import java.util.HashMap;
public class FileManager {
private static HashMap<String, String> mimeTypes = new HashMap<>();
private static boolean mimeTypesInitCompleted = false;
/**
* Diese HashMap beinhaltet einige Dateiendungen, die vom Webserver nicht mit dem Standard-MimeType ausgeliefert werden sollen
*
* @return Alle konfigruierten Dateiendungen mit ihren MimeTypes
*/
public static HashMap<String, String> getMimeTypes() {
if (mimeTypesInitCompleted) return mimeTypes;
// Bilder
mimeTypes.put(".gif", "image/gif");
mimeTypes.put(".jpg", "image/jpeg");
mimeTypes.put(".jpeg", "image/jpeg");
mimeTypes.put(".png", "image/png");
// Audio
mimeTypes.put(".mp3", "audio/mpeg");
mimeTypes.put(".mp4", "video/mp4");
mimeTypes.put(".flv", "video/x-flv");
// Webseiten
mimeTypes.put(".html", "text/html");
mimeTypes.put(".htm", "text/html");
mimeTypes.put(".shtml", "text/html");
mimeTypes.put(".xhtml", "text/html");
mimeTypes.put(".css", "text/css");
mimeTypes.put(".js", "text/js");
// Anderes
mimeTypes.put(".txt", "text/plain");
mimeTypes.put(".log", "text/plain");
mimeTypes.put(".md", "text/x-markdown");
mimeTypes.put(".pdf", "application/pdf");
mimeTypes.put(".xml", "application/xml");
mimeTypes.put(".java", "text/plain");
mimeTypesInitCompleted = true;
return mimeTypes;
}
/**
* Wandelt die Dateigröße in einen lesbaren Wert (mit entsprechender Einheit) um
* Quelle: Mr Ed: Format file size as MB, GB etc, 08.04.2011
* http://stackoverflow.com/questions/3263892/format-file-size-as-mb-gb-etc, 18.01.2014
*
* @param size Dateigröße (in Bits)
* @return Lesbare Dateigröße
*/
public static String getReadableFileSize(long size) {
if (size <= 0) return "0";
final String[] units = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB"};
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
}
/**
* Gibt den Content-Type (Mime-Type) für die entsprechende Dateiendung zurück
*
* @param file Zu prüfende Datei
* @return Content-Type der Datei
*/
public static String getContentType(File file) {
return getMimeTypes().get(getFileExtension(file));
}
/**
* Gibt die Dateiendung der gewählten Datei zurück (zB: "txt")
*
* @param file Zu prüfende Datei
* @return Dateiendung
*/
private static String getFileExtension(File file) {
String filename = file.getName();
int pos = filename.lastIndexOf(".");
if (pos >= 0) return filename.substring(pos).toLowerCase();
return "";
}
}
package de.menzerath.httpserver;
import de.menzerath.util.Logger;
import de.menzerath.util.ServerHelper;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
public class HTTPServer {
/**
* Einstiegspunkt der Anwendung; erstellt ein HTTPServer-Objekt
* @param args Beim Aufruf übergebene Argumente; werden aber ignoriert
*/
public static void main(String[] args) {
new HTTPServer(8080, new File("./www"), true, new File("log.txt"));
}
/**
* Konstruktor; erstellt (wenn nötig) den Ordner für die Daten und startet schließlich den ConnectionListener
*/
public HTTPServer(int port, final File webRoot, final boolean allowDirectoryListing, File logfile) {
Logger.setLogfile(logfile);
// Gib die IP-Adresse sowie den Port des Servers aus
// Passe die Ausgabe an die Länge der IP-Adresse + Port an
String lineOne = "";
String lineUrl = "### http://" + ServerHelper.getServerIp() + ":" + port + " ###";
String lineTitle = "### HTTP-Server";
for (int i = 0; i < lineUrl.length(); i++) lineOne += "#";
for (int i = 0; i < lineUrl.length() - 18; i++) lineTitle += " ";
lineTitle += "###";
// Ausgabe der Informationen
System.out.println(lineOne);
System.out.println(lineTitle);
System.out.println(lineUrl);
System.out.println(lineOne);
// Erstellt einen Ordner für die Daten (falls nötig)
if (!webRoot.exists() && !webRoot.mkdir()) {
// Ordner existiert nicht & konnte nicht angelegt werden: Abbruch
Logger.exception("Konnte Daten-Verzeichnis nicht erstellen.");
Logger.exception("Beende...");
System.exit(1);
}
// Erstelle einen ServerSocket mit dem angegebenen Port
ServerSocket socket = null;
try {
socket = new ServerSocket(port);
} catch (IOException | IllegalArgumentException e) {
// Port bereits belegt, darf nicht genutzt werden, ...: Abbruch
Logger.exception(e.getMessage());
Logger.exception("Beende...");
System.exit(1);
}
// Neuer Thread: wartet auf eingehende Verbindungen und "vermittelt" diese an einen neuen HTTPThread, der die Anfrage dann verarbeitet
final ServerSocket finalSocket = socket;
Thread connectionListener = new Thread(){
public void run(){
while (true) {
try {
HTTPThread thread = new HTTPThread(finalSocket.accept(), webRoot, allowDirectoryListing);
thread.start();
} catch (IOException e) {
Logger.exception(e.getMessage());
Logger.exception("Beende...");
System.exit(1);
}
}
}
};
connectionListener.start();
}
}
package de.menzerath.httpserver;
import de.menzerath.util.ServerHelper;
import de.menzerath.util.FileManager;
import de.menzerath.util.Logger;
import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.*;
public class HTTPThread extends Thread {
private Socket socket;
private File webRoot;
private boolean allowDirectoryListing;
/**
* Konstruktor; speichert die übergebenen Daten
*
* @param socket verwendeter Socket
* @param webRoot Pfad zum Hauptverzeichnis
* @param allowDirectoryListing Sollen Verzeichnisinhalte aufgelistet werden, falls keine Index-Datei vorliegt?
*/
public HTTPThread(Socket socket, File webRoot, boolean allowDirectoryListing) {
this.socket = socket;
this.webRoot = webRoot;
this.allowDirectoryListing = allowDirectoryListing;
}
/**
* "Herz" des Servers: Verarbeitet den Request des Clients, und sendet schließlich die Response
*/
public void run() {
// Vorbereitung und Einrichtung des BufferedReader und BufferedOutputStream
// zum Lesen des Requests und zur Ausgabe der Response
final BufferedReader in;
final BufferedOutputStream out;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF8"));
out = new BufferedOutputStream(socket.getOutputStream());
} catch (IOException e) {
Logger.exception(e.getMessage());
return;
}
// Timeout für die Verbindung von 30 Sekunden
// Verhindert zu viele offengehaltene Verbindungen, aber auch Übertragung von großen Dateien
try {
socket.setSoTimeout(30000);
} catch (SocketException e) {
Logger.exception(e.getMessage());
}
// Lesen des Request
String line;
ArrayList<String> request = new ArrayList<>();
try {
while ((line = in.readLine()) != null && line.length() != 0 && !line.equals("")) {
request.add(line);
}
} catch (IOException e) {
// Request konnte nicht (korrekt) gelesen werden
sendError(out, 400, "Bad Request");
Logger.exception(e.getMessage());
return;
}
// Request war leer; sollte nicht auftreten
if (request.isEmpty()) return;
// Nur Requests mit dem HTTP 1.0 / 1.1 Protokoll erlaubt
if (!request.get(0).endsWith(" HTTP/1.0") && !request.get(0).endsWith(" HTTP/1.1")) {
sendError(out, 400, "Bad Request");
Logger.error(400, "Bad Request: " + request.get(0), socket.getInetAddress().toString());
return;
}
// Es muss ein GET- oder POST-Request sein
boolean isPostRequest = false;
if (!request.get(0).startsWith("GET ")) {
if (request.get(0).startsWith("POST ")) {
// POST-Requests werden gesondert behandelt
isPostRequest = true;
} else {
// Methode nicht implementiert oder unbekannt
sendError(out, 501, "Not Implemented");
Logger.error(501, "Not Implemented: " + request.get(0), socket.getInetAddress().toString());
return;
}
}
// Auf welche Datei / welchen Pfad wird zugegriffen?
String wantedFile;
String path;
File file;
// GET-Request ist wahrscheinlicher, daher wird zuerst diese Methode angenommen
wantedFile = request.get(0).substring(4, request.get(0).length() - 9);
if (isPostRequest) wantedFile = request.get(0).substring(5, request.get(0).length() - 9);
// GET-Request mit Argumenten: Entferne diese für die Pfad-Angabe
if (!isPostRequest && request.get(0).contains("?")) {
path = wantedFile.substring(0, wantedFile.indexOf("?"));
} else {
path = wantedFile;
}
// Bestimme nun die exakte Datei, bzw. das Verzeichnis, welche(s) angefordert wurde
try {
file = new File(webRoot, URLDecoder.decode(path, "UTF-8")).getCanonicalFile();
} catch (IOException e) {
Logger.exception(e.getMessage());
return;
}
// Falls ein Verzeichnis angezeigt werden soll, und eine Index-Datei vorhanden ist
// soll letztere angezeigt werden
if (file.isDirectory()) {
File indexFile = new File(file, "index.html");
if (indexFile.exists() && !indexFile.isDirectory()) {
file = indexFile;
// "/index.html" an Verzeichnispfad anhängen
if (wantedFile.contains("?")) {
wantedFile = wantedFile.substring(0, wantedFile.indexOf("?")) + "/index.html" + wantedFile.substring(wantedFile.indexOf("?"));
}
}
}
if (!file.toString().startsWith(ServerHelper.getCanonicalWebRoot(webRoot))) {
// Datei liegt nicht innerhalb des Web-Roots: Zugriff verhindern und
// Fehlerseite senden
sendError(out, 403, "Forbidden");
Logger.error(403, wantedFile, socket.getInetAddress().toString());
return;
} else if (!file.exists()) {
// Datei existiert nicht: Fehlerseite senden
sendError(out, 404, "Not Found");
Logger.error(404, wantedFile, socket.getInetAddress().toString());
return;
} else if (file.isDirectory()) {
// Innerhalb eines Verzeichnis: Auflistung aller Dateien
// Verzeichnisauflistung verboten?
if (!allowDirectoryListing) {
// Fehlermeldung senden
sendError(out, 403, "Forbidden");
Logger.error(403, wantedFile, socket.getInetAddress().toString());
return;
}
// Ersetze alle "%20"-Leerzeichen mit einem "echten" Leerzeichen
path = path.replace("%20", " ");
File[] files = file.listFiles();
// Das Verzeichnis ist leer? Sende eine entsprechende Fehlermeldung
if (files != null) {
if (files.length == 0) {
sendError(out, 404, "Not Found");
Logger.error(404, wantedFile, socket.getInetAddress().toString());
return;
}
} else {
// Kann unter Umständen auf Windows-Systemen vorkommen
// Beispiel: Aufruf von "Documents and Settings" anstelle von "Users"
sendError(out, 403, "Forbidden");
Logger.error(403, wantedFile, socket.getInetAddress().toString());
return;
}
// Alle Einträge alphabetisch sortieren: Zuerst Ordner, danach Dateien
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
if (f1.isDirectory() && !f2.isDirectory()) {
return -1;
} else if (!f1.isDirectory() && f2.isDirectory()) {
return 1;
} else {
return f1.toString().compareToIgnoreCase(f2.toString());
}
}
});
// Ausgabe in einer Tabelle vorbereiten
String content = "<table><tr><th></th><th>Name</th><th>Last modified</th><th>Size</th></tr>";
// Einen "Ebene höher"-Eintrag anlegen, falls nicht im Web-Root gearbeitet wird
if (!path.equals("/")) {
String parentDirectory = path.substring(0, path.length() - 1);
int lastSlash = parentDirectory.lastIndexOf("/");
if (lastSlash > 1) {
parentDirectory = parentDirectory.substring(0, lastSlash);
} else {
parentDirectory = "/";
}
content += "<tr><td class=\"center\"><div class=\"back\">&nbsp;</div></td>" +
"<td><a href=\"" + parentDirectory.replace(" ", "%20") + "\">Parent Directory</a></td>" +
"<td></td>" +
"<td></td></tr>";
}
if (path.equals("/")) path = ""; // Anpassung für Dateiauflistung
// Jede Datei zur Ausgabe hinzufügen
for (File myFile : files) {
// Meta-Daten der Datei abrufen
String filename = myFile.getName();
String img;
String fileSize = FileManager.getReadableFileSize(myFile.length());
if (myFile.isDirectory()) {
img = "<div class=\"folder\">&nbsp;</div>";
fileSize = "";
} else {
img = "<div class=\"file\">&nbsp;</div>";
}
// Datei in die Tabelle einfügen
content += "<tr><td class=\"center\">" + img + "</td>" +
"<td><a href=\"" + path.replace(" ", "%20") + "/" + filename.replace(" ", "%20") + "\">" + filename + "</a></td>" +
"<td>" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(myFile.lastModified()) + "</td>" +
"<td class=\"center\">" + fileSize + "</td></tr>";
}
if (path.equals("")) path = "/"; // Rück-Anpassung für spätere Verwendung
// Tabelle schließen und mit Template zusammenfügen
content += "</table>";
String output = WebResources.getDirectoryTemplate("Index of " + path, content);
// Abschließenden Slash an Verzeichnis anhängen
if (!wantedFile.endsWith("/")) wantedFile += "/";
// Header und Inhalt senden
sendHeader(out, 200, "OK", "text/html", -1, System.currentTimeMillis());
Logger.access(wantedFile, socket.getInetAddress().toString());
try {
out.write(output.getBytes());
} catch (IOException e) {
Logger.exception(e.getMessage());
}
} else {
// Eine einzelne Datei wurde angefordert: Ausgabe via InputStream
// InputStream vorbereiten
InputStream reader = null;
try {
reader = new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
Logger.exception(e.getMessage());
}
// Datei existiert (erstaunlicherweise) nicht (mehr)
if (reader == null) {
sendError(out, 404, "Not Found");
Logger.error(404, wantedFile, socket.getInetAddress().toString());
return;
}
// Falls es keinen festgelegten ContentType zur Dateiendung gibt, wird der Download gestartet
String contentType = FileManager.getContentType(file);
if (contentType == null) {
contentType = "application/octet-stream";
}
// Header senden, Zugriff loggen und Datei senden
sendHeader(out, 200, "OK", contentType, file.length(), file.lastModified());
Logger.access(wantedFile, socket.getInetAddress().toString());
try {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = reader.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
reader.close();
} catch (NullPointerException | IOException e) {
// Wirft eine "Broken Pipe" oder "Socket Write Error" Exception,
// wenn der Download / Stream abgebrochen wird
Logger.exception(e.getMessage());
}
}
// OutputStream leeren und schließen
try {
out.flush();
out.close();
} catch (IOException e) {
Logger.exception(e.getMessage());
}
}
/**
* Sende den HTTP 1.1 Header zum Client
*
* @param out Genutzter OutputStream
* @param code Status-Code, der gesendet werden soll
* @param codeMessage Zum Status-Code gehörende Nachricht
* @param contentType ContentType des Inhalts
* @param contentLength Größe des Inhalts
* @param lastModified Wann die Datei zuletzt verändert wurde (zum Caching des Browsers)
*/
private void sendHeader(BufferedOutputStream out, int code, String codeMessage, String contentType, long contentLength, long lastModified) {
try {
out.write(("HTTP/1.1 " + code + " " + codeMessage + "\r\n" +
"Date: " + new Date().toString() + "\r\n" +
"Server: Marvins HTTP-Server\r\n" +
"Content-Type: " + contentType + "; charset=utf-8\r\n" +
((contentLength != -1) ? "Content-Length: " + contentLength + "\r\n" : "") +
"Last-modified: " + new Date(lastModified).toString() + "\r\n" +
"\r\n").getBytes());
} catch (IOException e) {
Logger.exception(e.getMessage());
}
}
/**
* Sendet eine Fehlerseite zum Browser
*
* @param out Genutzter OutputStream
* @param code Fehler-Code, der gesendet werden soll (403, 404, ...)
* @param message Zusätzlicher Text ("Not Found", ...)
*/
private void sendError(BufferedOutputStream out, int code, String message) {
// Bereitet Daten der Response vor
String output = WebResources.getErrorTemplate("Error " + code + ": " + message);
// Sendet Header der Response
sendHeader(out, code, message, "text/html", output.length(), System.currentTimeMillis());
try {
// Sendet Daten der Response
out.write(output.getBytes());
out.flush();
out.close();
// Schließt den Socket; "keep-alive" wird also ignoriert
socket.close();
} catch (IOException e) {
Logger.exception(e.getMessage());
}
}
}
package de.menzerath.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
public class Logger {
private static File logfile;
/**
* Ändert die Datei, in der alle Log-Meldungen gespeichert werden
*
* @param pLogfile Neue Log-Datei
*/
public static void setLogfile(File pLogfile) {
logfile = pLogfile;
}
/**
* Zugriff auf eine Datei / ein Verzeichnis
*
* @param file Datei auf die zugegriffen wurde
* @param ip IP-Adresse des Clients
*/
public static void access(String file, String ip) {
write("[200] [" + ip.replace("/", "") + "] " + file);
}
/**
* Fehler beim Zugriff (403, 404, ...)
*
* @param code HTTP-Status-Code
* @param file Datei auf die zugegriffen werden sollte
* @param ip IP-Adresse des Clients
*/
public static void error(int code, String file, String ip) {
write("[" + code + "] [" + ip.replace("/", "") + "] " + file);
}
/**
* Interner Fehler (Abbruch eines Streams, ...)
*
* @param message Inhalt / Grund der Exception
*/
public static void exception(String message) {
write("[EXC] " + message);
}
/**
* Gibt die Log-Meldung auf der Konsole aus und speichert sie in der Log-Datei
*
* @param message Inhalt der Meldung
*/
private static void write(String message) {
// Zusammensetzung der Meldung
String out = "[" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()) + "] " + message;
// Ausgabe auf der Konsole
System.out.println(out);
// Schreibe in Datei
try {
PrintWriter printWriter = new PrintWriter(new FileOutputStream(logfile, true));
printWriter.append(out).append("\r\n");
printWriter.close();
} catch (IOException ignored) {}
}
}
package de.menzerath.util;
import java.io.File;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.UnknownHostException;
public class ServerHelper {
/**
* Gibt die IP-Adresse des Servers im lokalen Netzwerk zurück
* @return IP-Adresse des Servers
*/
public static String getServerIp() {
try {
return Inet4Address.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
Logger.exception(e.getMessage());
return "127.0.0.1";
}
}
/**
* Gibt den "anerkannten" Webroot des Servers zurück; dh. einen validen und absoluten Pfad zum Ordner
* @return Pfad zum Webroot
*/
public static String getCanonicalWebRoot(File webRoot) {
String canonicalWebRoot = "";
try {
canonicalWebRoot = webRoot.getCanonicalPath();
} catch (IOException e) {
Logger.exception(e.getMessage());
}
return canonicalWebRoot;
}
}
package de.menzerath.httpserver;
/**
* Diese Klasse beinhaltet Ressourcen, die bei der Dateiauflistung und
* bei Fehlerseien angezeigt werden:
* CSS, Header, Footer
*
* Icons (base64-encodiert) in der Auflistung der Dateien: erstellt von Yannick Lung (yanlu.de)
*/
public class WebResources {
private static final String STYLE =
"html * { font-family: sans-serif;!important; }" +
"table { border-collapse: collapse; margin-bottom: 1em; }" +
"th { background: #70a0b2; color: #fff; }" +
"td, tbody th { border: 1px solid #e1e1e1; font-size: 15px; padding: .5em .3em; }" +
"tr:hover td { background: #e9edf1 }" +
"td.center { text-align: center; }" +
"div.folder { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAAeUlEQVQ4je3UMQqDUAyA4e8VBw/RS/QKHbxPr+HiJUrv0Cu5OBREEKyLg/g6iD6hgz8EkhB+Qobw74RFfUex6LUo0a0RZrP8igfyH3MfVOt2PIAXenx3Ro9nwCC+5VaGMNmTcUkpO4WncIewSeirA27il7WVdyLPgYxpHR5nWoKFpAAAAABJRU5ErkJggg==); background-position: center center; background-repeat:no-repeat; height:20px; width:20px; }" +
"div.file { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAA3klEQVQ4je3TvUoDQRSG4cdgIekEQfACbC0s05pC8Dr2EqxTWlta5AZs7BMk9qb0BmxNEdjGxp/CWZgMM7tZSJHCDw4cds687MzLsOMcRH0VqisPeMYaq3TxMOrPcBw2lFKFuVPc4wbfJSDUWLYA66i/xi3uSsMT/GxRE4xC/4VxDBkk0IW/ey3VIpkfYIqT5kN65Eu8lo6Ac7zgLfmzYQn4gccWYIUrHGXW3nPAbaSMQsX5xDwHvMCsBdiZvlI6RfWVkksjKgvskpLLxnPtKyWX+PXsTMpT0/xL2UMpv3j7OZHPExvvAAAAAElFTkSuQmCC); background-position: center center; background-repeat:no-repeat; height:20px; width:20px; }" +
"div.back { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAA5ElEQVQ4je3UsUpDQRCF4Y+YQAqDIIgQIYVdCFZaikVAiI29teQJItha+gB2eQMLwUZEfAJLWyEWViGNWAQCYlJchWW5JnchnR4YWPbM/Mzsssu/IlVTC0pzvHOcLQNYQR+XKKcC44IN3GA/FfSjlWC9jQfsBnufqGMPDaxiiK9F4AOMMC0QH7jG4W+wU0wKwuK4w1YIK2EtGj1FR3hCK894X9DNGAO8yc429IbYjKFNvESJ9+jIbj9UBTvo4fk79zZvhHU8BsCLAmPDMV7RzjPLuEoEQg3deQknsue3VCV/Dn9QMzw6REIVdJIPAAAAAElFTkSuQmCC); background-position: center center; background-repeat:no-repeat; height:20px; width:20px; }";
public static String getDirectoryTemplate(String title, String content) {
return "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" +
"<link rel=\"icon\" type=\"image/png\" href=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAAeUlEQVQ4je3UMQqDUAyA4e8VBw/RS/QKHbxPr+HiJUrv0Cu5OBREEKyLg/g6iD6hgz8EkhB+Qobw74RFfUex6LUo0a0RZrP8igfyH3MfVOt2PIAXenx3Ro9nwCC+5VaGMNmTcUkpO4WncIewSeirA27il7WVdyLPgYxpHR5nWoKFpAAAAABJRU5ErkJggg==\" />" +
"<style>" + STYLE + "</style>" +
"<title>" + title + "</title>" +
"</head>" +
"<body>" +
"<h1>" + title + "</h1>" +
content +
"</body>" +
"</html>";
}
public static String getErrorTemplate(String error) {
return "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<title>" + error + "</title>" +
"</head>" +
"<body>" +
"<h1>" + error + "</h1>" +
"</body>" +
"</html>";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment