From c3cae930ce1f814424004489829ccbf2bf4843bb Mon Sep 17 00:00:00 2001 From: Lukas-Heiligenbrunner <30468956+Lukas-Heiligenbrunner@users.noreply.github.com> Date: Sat, 29 Feb 2020 10:38:37 +0100 Subject: [PATCH] init --- build.gradle | 67 +++++++++ settings.gradle | 1 + src/java/eu/heili/hometheater/Main.kt | 41 ++++++ .../eu/heili/hometheater/basicutils/Info.java | 85 +++++++++++ .../eu/heili/hometheater/basicutils/Log.kt | 139 ++++++++++++++++++ .../eu/heili/hometheater/db/Database.java | 25 ++++ src/java/eu/heili/hometheater/db/JDBC.java | 111 ++++++++++++++ .../heili/hometheater/db/MySQLConnector.java | 29 ++++ .../eu/heili/hometheater/website/HttpTools.kt | 32 ++++ .../eu/heili/hometheater/website/MainPage.kt | 56 +++++++ .../eu/heili/hometheater/website/Webserver.kt | 27 ++++ .../website/basicrequest/GetRequest.kt | 38 +++++ .../website/basicrequest/PostRequest.kt | 44 ++++++ .../datarequests/login/LoginState.java | 61 ++++++++ src/resources/wwwroot/index.html | 10 ++ 15 files changed, 766 insertions(+) create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/java/eu/heili/hometheater/Main.kt create mode 100644 src/java/eu/heili/hometheater/basicutils/Info.java create mode 100644 src/java/eu/heili/hometheater/basicutils/Log.kt create mode 100755 src/java/eu/heili/hometheater/db/Database.java create mode 100644 src/java/eu/heili/hometheater/db/JDBC.java create mode 100755 src/java/eu/heili/hometheater/db/MySQLConnector.java create mode 100644 src/java/eu/heili/hometheater/website/HttpTools.kt create mode 100644 src/java/eu/heili/hometheater/website/MainPage.kt create mode 100644 src/java/eu/heili/hometheater/website/Webserver.kt create mode 100644 src/java/eu/heili/hometheater/website/basicrequest/GetRequest.kt create mode 100644 src/java/eu/heili/hometheater/website/basicrequest/PostRequest.kt create mode 100644 src/java/eu/heili/hometheater/website/datarequests/login/LoginState.java create mode 100644 src/resources/wwwroot/index.html diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..da88389 --- /dev/null +++ b/build.gradle @@ -0,0 +1,67 @@ +import java.text.SimpleDateFormat + +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.3.61' +} + +group 'eu.heili.hometheater' +version '0.0.1-Beta' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + manifest { + attributes 'Main-Class': 'eu.heili.hometheater.Main' + } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} + +sourceSets { + main.java.srcDirs = ['src/java'] + main.resources.srcDirs = ['src/resources'] +} + +dependencies { + compile group: 'org.eclipse.paho', name: 'org.eclipse.paho.client.mqttv3', version: '1.2.2' + compile group: 'mysql',name:'mysql-connector-java',version: '8.0.18' + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +} + +task run (type: JavaExec){ + description = "Secure algorythm testing" + main = 'eu.heili.hometheater.Main' + classpath = sourceSets.main.runtimeClasspath +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$projectDir/src/resources/version.properties").withWriter { w -> + Properties p = new Properties() + p['version'] = project.version.toString() + p['buildtime'] = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date()) + p.store w, null + } + } +} + +task myJavadocs(type: Javadoc) { + title = "JAVADOC hometheater" + source = sourceSets.main.allJava + classpath = sourceSets.main.runtimeClasspath +} + +classes { + dependsOn createProperties +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..dd2d134 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Home-Theater' \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/Main.kt b/src/java/eu/heili/hometheater/Main.kt new file mode 100644 index 0000000..87f5dee --- /dev/null +++ b/src/java/eu/heili/hometheater/Main.kt @@ -0,0 +1,41 @@ +@file:JvmName("Main") +package eu.heili.hometheater + +import eu.heili.hometheater.basicutils.Info +import eu.heili.hometheater.basicutils.Log +import eu.heili.hometheater.db.JDBC +import eu.heili.hometheater.website.Webserver +import java.io.IOException + +fun main() { + Log.setLevel(Log.DEBUG) + Info.init() + + Log.info("startup of Home-Theater") + + Runtime.getRuntime().addShutdownHook(Thread(Runnable { + try { + Thread.sleep(200) + Log.warning("Shutting down ...") + //shutdown routine + } catch (e: InterruptedException) { + e.printStackTrace() + } + })) + + Log.info("Server version: " + Info.getVersion()) + Log.debug("Build date: " + Info.getBuilddate()) + + //initial connect to db + Log.message("initial login to db") + try { + JDBC.init("todo", "todo", "todo", "todo.heili.eu", 3306) + } catch (e: IOException) { //e.printStackTrace(); + Log.error("no connection to db") + } + + + //startup web server + val mythread = Thread(Runnable { Webserver().startserver() }) + mythread.start() +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/basicutils/Info.java b/src/java/eu/heili/hometheater/basicutils/Info.java new file mode 100644 index 0000000..ed72ecb --- /dev/null +++ b/src/java/eu/heili/hometheater/basicutils/Info.java @@ -0,0 +1,85 @@ +package eu.heili.hometheater.basicutils; + +import java.io.IOException; +import java.net.URL; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Properties; + +/** + * get basic infos about Software + * + * @author Lukas Heiligenbrunner + */ +public class Info { + private static String version = "not init"; + private static String builddate = "not init"; + private static String starttime = "not init"; + + /** + * get Software Version (defined in gradle build file) + * + * @return Version as string + */ + public static String getVersion() { + return version; + } + + /** + * get Software build date + * + * @return Date as string + */ + public static String getBuilddate() { + return builddate; + } + + /** + * get Server start time + * + * @return start time + */ + public static String getStarttime() { + return starttime; + } + + /** + * initialize the version and builddate variables + */ + public static void init() { + starttime = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date()); + Properties prop = new Properties(); + try { + URL url = Info.class.getResource("/version.properties"); + + prop.load(url.openStream()); + version = (String) prop.get("version"); + builddate = (String) prop.get("buildtime"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * print memory utilization + * todo parse into website somehow + */ + public static void getMemoryUsage() { + Runtime runtime = Runtime.getRuntime(); + + NumberFormat format = NumberFormat.getInstance(); + + StringBuilder sb = new StringBuilder(); + long maxMemory = runtime.maxMemory(); + long allocatedMemory = runtime.totalMemory(); + long freeMemory = runtime.freeMemory(); + + sb.append("free memory: " + format.format(freeMemory / 1024) + "\n"); + sb.append("allocated memory: " + format.format(allocatedMemory / 1024) + "\n"); + sb.append("max memory: " + format.format(maxMemory / 1024) + "\n"); + sb.append("total free memory: " + format.format((freeMemory + (maxMemory - allocatedMemory)) / 1024) + "\n"); + + System.out.println(sb.toString()); + } +} diff --git a/src/java/eu/heili/hometheater/basicutils/Log.kt b/src/java/eu/heili/hometheater/basicutils/Log.kt new file mode 100644 index 0000000..71154f6 --- /dev/null +++ b/src/java/eu/heili/hometheater/basicutils/Log.kt @@ -0,0 +1,139 @@ +package eu.heili.hometheater.basicutils + +import java.text.SimpleDateFormat +import java.util.* + +class Log { + companion object Log{ + val CRITICAL_ERROR = 6 + val ERROR = 5 + val WARNING = 4 + val INFO = 3 + val MESSAGE = 2 + val DEBUG = 1 + + private val ANSI_RESET = "\u001B[0m" + private val ANSI_BLACK = "\u001B[30m" + private val ANSI_RED = "\u001B[31m" + private val ANSI_GREEN = "\u001B[32m" + private val ANSI_YELLOW = "\u001B[33m" + private val ANSI_BLUE = "\u001B[34m" + private val ANSI_PURPLE = "\u001B[35m" + private val ANSI_CYAN = "\u001B[36m" + private val ANSI_WHITE = "\u001B[37m" + + private var Loglevel = 0 + + /** + * Log critical Error + * + * @param msg message + */ + fun criticalerror(msg: Any) { + if (Loglevel <= CRITICAL_ERROR) log(msg, CRITICAL_ERROR) + } + + /** + * Log basic Error + * + * @param msg message + */ + fun error(msg: Any) { + if (Loglevel <= ERROR) log(msg, ERROR) + } + + /** + * Log warning + * + * @param msg message + */ + fun warning(msg: Any) { + if (Loglevel <= WARNING) log(msg, WARNING) + } + + /** + * Log info + * + * @param msg message + */ + fun info(msg: Any) { + if (Loglevel <= INFO) log(msg, INFO) + } + + /** + * Log basic message + * + * @param msg message + */ + fun message(msg: Any) { + if (Loglevel <= MESSAGE) log(msg, MESSAGE) + } + + /** + * Log debug Message + * + * @param msg message + */ + fun debug(msg: Any) { + if (Loglevel <= DEBUG) log(msg, DEBUG) + } + + /** + * Log as defined + * + * @param msg message + * @param level Loglevel --> static vals defined + */ + fun log(msg: Any, level: Int) { + val iswindows = System.getProperty("os.name").contains("Windows") + val builder = StringBuilder() + if (!iswindows) { + when (level) { + INFO -> builder.append(ANSI_CYAN) + WARNING -> builder.append(ANSI_YELLOW) + ERROR -> builder.append(ANSI_RED) + CRITICAL_ERROR -> builder.append(ANSI_RED) + MESSAGE -> builder.append(ANSI_WHITE) + DEBUG -> builder.append(ANSI_BLUE) + } + } + builder.append("[") + builder.append(calcDate(System.currentTimeMillis())) + builder.append("]") + builder.append(" [") + builder.append(Exception().stackTrace[2].className) + builder.append("]") + builder.append(" [") + builder.append(colors[level]) + builder.append("]") + if (!iswindows) { + builder.append(ANSI_WHITE) + } + builder.append(" - ") + builder.append(msg.toString()) + if (!iswindows) { + builder.append(ANSI_RESET) + } + println(builder.toString()) + } + + /** + * define Loglevel call on startup or at runtime + * default: 0[DEBUG] --> Max logging + * + * @param level Loglevel --> static vals defined + */ + fun setLevel(level: Int) { + Loglevel = level + } + private val colors = ArrayList(Arrays.asList("", "DEBUG", "MESSAGE", "INFO", "WARNING", "ERROR", "CRITICAL_ERROR")) + + + + private fun calcDate(millisecs: Long): String? { + val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + val resultdate = Date(millisecs) + return date_format.format(resultdate) + } + } +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/db/Database.java b/src/java/eu/heili/hometheater/db/Database.java new file mode 100755 index 0000000..6e25950 --- /dev/null +++ b/src/java/eu/heili/hometheater/db/Database.java @@ -0,0 +1,25 @@ +package eu.heili.hometheater.db; + +import java.sql.Connection; +import java.sql.SQLException; + +abstract class Database { + + protected String user; + protected String password; + + protected String host; + protected int port; + + protected String dbName; + + public Database(String user, String password, String host, int port, String dbName) { + this.user = user; + this.password = password; + this.host = host; + this.port = port; + this.dbName = dbName; + } + + public abstract Connection getConnection() throws SQLException; +} diff --git a/src/java/eu/heili/hometheater/db/JDBC.java b/src/java/eu/heili/hometheater/db/JDBC.java new file mode 100644 index 0000000..8d242bd --- /dev/null +++ b/src/java/eu/heili/hometheater/db/JDBC.java @@ -0,0 +1,111 @@ +package eu.heili.hometheater.db; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * basic connection class to a Database + * + * @author Lukas Heiligenbrunner + */ +public class JDBC { + private static Connection conn; + + private static JDBC JDBC; + private static boolean loggedin = false; + + private static String usernamec; + private static String passwordc; + private static String dbnamec; + private static String ipc; + private static int portc; + + /** + * initialize database values + * suggested on startup + * + * @param username db username + * @param password db password + * @param dbname Database name + * @param ip Server ip or hostname + * @param port Server port + * @throws IOException + */ + public static void init(String username, String password, String dbname, String ip, int port) throws IOException { + usernamec = username; + passwordc = password; + dbnamec = dbname; + ipc = ip; + portc = port; + JDBC = new JDBC(username, password, dbname, ip, port); + } + + private JDBC(String username, String password, String dbname, String ip, int port) throws IOException { + logintodb(username, password, dbname, ip, port); + } + + /** + * get instance of db object + * logindata has to be set before! + * + * @return JDBC object of this + * @throws IOException + */ + public static JDBC getInstance() throws IOException { + if (loggedin) { + return JDBC; + } else { + logintodb(usernamec, passwordc, dbnamec, ipc, portc); + return JDBC; + } + + } + + private static void logintodb(String username, String password, String dbname, String ip, int port) throws IOException { + Database db = new MySQLConnector( + username, + password, + ip, + port, + dbname); + + try { + conn = db.getConnection(); + loggedin = true; + } catch (SQLException e) { + throw new IOException("No connection to database"); + } + + } + + /** + * execute basic query --> requests only + * + * @param sql query sql statement + * @return ResultSet representating the table + */ + public ResultSet executeQuery(String sql) { + try { + PreparedStatement stmt = conn.prepareStatement(sql); + return stmt.executeQuery(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + /** + * update db in some way + * + * @param sql sql insert/update/delete statement + * @return status + * @throws SQLException + */ + public int executeUpdate(String sql) throws SQLException { + PreparedStatement stmt = conn.prepareStatement(sql); + + return stmt.executeUpdate(); + } +} diff --git a/src/java/eu/heili/hometheater/db/MySQLConnector.java b/src/java/eu/heili/hometheater/db/MySQLConnector.java new file mode 100755 index 0000000..630b2df --- /dev/null +++ b/src/java/eu/heili/hometheater/db/MySQLConnector.java @@ -0,0 +1,29 @@ +package eu.heili.hometheater.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +class MySQLConnector extends Database { + + static { + try { + Class.forName("com.mysql.cj.jdbc.Driver").getDeclaredConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public MySQLConnector(String user, String password, String host, int port, String dbName) { + super(user, password, host, port, dbName); + } + + public Connection getConnection() throws SQLException { + DriverManager.setLoginTimeout(1); + return DriverManager.getConnection( + "jdbc:mysql://" + host + ":" + port + "/" + dbName + "?useSSL=false", + user, + password); + } + +} diff --git a/src/java/eu/heili/hometheater/website/HttpTools.kt b/src/java/eu/heili/hometheater/website/HttpTools.kt new file mode 100644 index 0000000..3bacb5a --- /dev/null +++ b/src/java/eu/heili/hometheater/website/HttpTools.kt @@ -0,0 +1,32 @@ +package eu.heili.hometheater.website + +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * basic http tools + * + * @author Lukas Heiligenbrunner + */ +class HttpTools { + companion object{ + /** + * create md5 hash of string + * + * @param value input string + * @return md5 hash + */ + fun StringToMD5(value: String): String { + return try { + val md = MessageDigest.getInstance("MD5") + val messageDigest = md.digest(value.toByteArray()) + val no = BigInteger(1, messageDigest) + no.toString(16) + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + "" + } + } + } +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/website/MainPage.kt b/src/java/eu/heili/hometheater/website/MainPage.kt new file mode 100644 index 0000000..f2527f7 --- /dev/null +++ b/src/java/eu/heili/hometheater/website/MainPage.kt @@ -0,0 +1,56 @@ +package eu.heili.hometheater.website + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpHandler +import eu.heili.hometheater.basicutils.Log.Log.debug +import eu.heili.hometheater.basicutils.Log.Log.warning +import eu.heili.hometheater.website.datarequests.login.LoginState +import java.io.IOException + +class MainPage : HttpHandler { + @Throws(IOException::class) + override fun handle(t: HttpExchange) { + var path = t.requestURI.path + if (path == "/") { + path += "index.html" + } + debug("looking for: $path") + if (path.contains(".html")) { + if (LoginState.getObject().isLoggedIn || path == "/register.html" || path == "/index.html") { //pass only register page + sendPage(path, t) + } else { + warning("user not logged in --> redirecting to login page") + sendPage("/index.html", t) + } + } else { //only detect login state on html pages + sendPage(path, t) + } + } + + @Throws(IOException::class) + private fun sendPage(path: String, t: HttpExchange) { + val fs = javaClass.getResourceAsStream("/wwwroot$path") + if (fs == null && path.contains(".html")) { + warning("wrong page sending 404") + sendPage("/404Error.html", t) + } else if (fs == null) { + warning("requested resource doesnt exist --> $path") + } else { // Object exists and is a file: accept with response code 200. + var mime = "text/html" + val s = path.substring(path.length - 3) + if (s == ".js") mime = "application/javascript" + if (s == "css") mime = "text/css" + val h = t.responseHeaders + h["Content-Type"] = mime + t.sendResponseHeaders(200, 0) + val os = t.responseBody + val buffer = ByteArray(0x10000) + var count: Int + while (fs.read(buffer).also { count = it } >= 0) { + os.write(buffer, 0, count) + } + fs.close() + os.close() + } + } +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/website/Webserver.kt b/src/java/eu/heili/hometheater/website/Webserver.kt new file mode 100644 index 0000000..ab3b385 --- /dev/null +++ b/src/java/eu/heili/hometheater/website/Webserver.kt @@ -0,0 +1,27 @@ +package eu.heili.hometheater.website + +import com.sun.net.httpserver.HttpServer +import eu.heili.hometheater.basicutils.Log.Log.criticalerror +import eu.heili.hometheater.basicutils.Log.Log.info +import java.io.IOException +import java.net.BindException +import java.net.InetSocketAddress + +class Webserver { + fun startserver() { + info("starting Webserver") + try { + val server = HttpServer.create(InetSocketAddress(8080), 0) + server.createContext("/", MainPage()) + // todo insert get and post request sites here! + server.executor = null // creates a default executor + server.start() + info("Server available at http://127.0.0.1:8080 now") + } catch (e: BindException) { + criticalerror("The Port 8080 is already in use!") + // todo option to choose other port + } catch (e: IOException) { + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/website/basicrequest/GetRequest.kt b/src/java/eu/heili/hometheater/website/basicrequest/GetRequest.kt new file mode 100644 index 0000000..b78434d --- /dev/null +++ b/src/java/eu/heili/hometheater/website/basicrequest/GetRequest.kt @@ -0,0 +1,38 @@ +package com.wasteinformationserver.website.basicrequest + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpHandler +import java.io.IOException +import java.util.* + +/** + * basic GET request handler + * reply function has to be implemented! + */ +abstract class GetRequest : HttpHandler { + @Throws(IOException::class) + override fun handle(httpExchange: HttpExchange) { + if (httpExchange.requestMethod == "GET") { + val query = httpExchange.requestURI.query + val params = HashMap() + val res = query.split("&".toRegex()).toTypedArray() + for (str in res) { + val values = str.split("=".toRegex()).toTypedArray() + params[values[0]] = values[1] + } + val response = myrequest(params) + val h = httpExchange.responseHeaders + h["Content-Type"] = "application/json" + httpExchange.sendResponseHeaders(200, 0) + val os = httpExchange.responseBody + os.write(response.toByteArray()) + os.close() + } + } + + /** + * @param params received get params from com.wasteinformationserver.website + * @return json reply to com.wasteinformationserver.website + */ + abstract fun myrequest(params: HashMap): String +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/website/basicrequest/PostRequest.kt b/src/java/eu/heili/hometheater/website/basicrequest/PostRequest.kt new file mode 100644 index 0000000..8edc8e2 --- /dev/null +++ b/src/java/eu/heili/hometheater/website/basicrequest/PostRequest.kt @@ -0,0 +1,44 @@ +package com.wasteinformationserver.website.basicrequest + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpHandler +import java.io.IOException +import java.util.* + +/** + * basic POST request handler + * reply function has to be implemented! + */ +abstract class PostRequest : HttpHandler { + @Throws(IOException::class) + override fun handle(httpExchange: HttpExchange) { + if (httpExchange.requestMethod == "POST") { + val sb = StringBuilder() + val ios = httpExchange.requestBody + var i: Int + while (ios.read().also { i = it } != -1) { + sb.append(i.toChar()) + } + val query = sb.toString() + val params = HashMap() + val res = query.split("&".toRegex()).toTypedArray() + for (str in res) { + val values = str.split("=".toRegex()).toTypedArray() + params[values[0]] = values[1] + } + val response = request(params) + val h = httpExchange.responseHeaders + h["Content-Type"] = "application/json" + httpExchange.sendResponseHeaders(200, 0) + val os = httpExchange.responseBody + os.write(response.toByteArray()) + os.close() + } + } + + /** + * @param params received get params from com.wasteinformationserver.website + * @return json reply to com.wasteinformationserver.website + */ + abstract fun request(params: HashMap): String +} \ No newline at end of file diff --git a/src/java/eu/heili/hometheater/website/datarequests/login/LoginState.java b/src/java/eu/heili/hometheater/website/datarequests/login/LoginState.java new file mode 100644 index 0000000..66fb89b --- /dev/null +++ b/src/java/eu/heili/hometheater/website/datarequests/login/LoginState.java @@ -0,0 +1,61 @@ +package eu.heili.hometheater.website.datarequests.login; + +/** + * @author Lukas Heiligenbrunner + */ +public class LoginState { + + private static LoginState mythis = new LoginState(); + + public static LoginState getObject() { + return mythis; + } + + private String username; + private String firstname; + private String lastname; + private String email; + private int permission; + + boolean loggedin = false; + + public void logIn() { + loggedin = true; + } + + public void logOut() { + loggedin = false; + } + + public void setAccountData(String username, String firstname, String lastname, String email, int permission) { + this.username = username; + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + this.permission = permission; + } + + public boolean isLoggedIn() { + return loggedin; + } + + public String getUsername() { + return username; + } + + public String getFirstname() { + return firstname; + } + + public String getLastname() { + return lastname; + } + + public String getEmail() { + return email; + } + + public int getPermission() { + return permission; + } +} diff --git a/src/resources/wwwroot/index.html b/src/resources/wwwroot/index.html new file mode 100644 index 0000000..900d476 --- /dev/null +++ b/src/resources/wwwroot/index.html @@ -0,0 +1,10 @@ + + + + + Home-Theater + + +main body + + \ No newline at end of file