Add: secure local HTML file loading in Casono Browser and website autocompletion
This commit is contained in:
+166
-14
@@ -4,7 +4,10 @@ import java.net.CookieHandler;
|
|||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
@@ -29,6 +32,10 @@ import javafx.scene.web.WebView;
|
|||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental embedded browser for Casono.
|
* Experimental embedded browser for Casono.
|
||||||
@@ -63,6 +70,11 @@ public class CasinoBrowserController {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
private static final Logger LOGGER = LogManager.getLogger(CasinoBrowserController.class);
|
||||||
|
|
||||||
|
private static final ObservableList<String> URL_SUGGESTIONS =
|
||||||
|
FXCollections.observableArrayList(TRUSTED_DOMAINS);
|
||||||
|
|
||||||
|
private static ContextMenu autoCompleteMenu = new ContextMenu();
|
||||||
|
|
||||||
private static final int LOGO_HEIGHT = 40;
|
private static final int LOGO_HEIGHT = 40;
|
||||||
private static final int CORNER_RADIUS = 40;
|
private static final int CORNER_RADIUS = 40;
|
||||||
private static final int HBOX_SPACIN = 10;
|
private static final int HBOX_SPACIN = 10;
|
||||||
@@ -79,7 +91,9 @@ public class CasinoBrowserController {
|
|||||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||||
|
|
||||||
TRUSTED_DOMAINS.add("wikipedia.org");
|
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||||
|
TRUSTED_DOMAINS.add("youtube.com");
|
||||||
TRUSTED_DOMAINS.add("github.com");
|
TRUSTED_DOMAINS.add("github.com");
|
||||||
|
TRUSTED_DOMAINS.add("search.brave.com");
|
||||||
TRUSTED_DOMAINS.add("oracle.com");
|
TRUSTED_DOMAINS.add("oracle.com");
|
||||||
TRUSTED_DOMAINS.add("stackoverflow.com");
|
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||||
TRUSTED_DOMAINS.add("docs.oracle.com");
|
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||||
@@ -98,6 +112,58 @@ public class CasinoBrowserController {
|
|||||||
TRUSTED_DOMAINS.add("startpage.com");
|
TRUSTED_DOMAINS.add("startpage.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aliases for common websites.
|
||||||
|
*/
|
||||||
|
private static final java.util.Map<String, String> ALIASES = Map.ofEntries(
|
||||||
|
Map.entry("wiki", "wikipedia.org"),
|
||||||
|
Map.entry("yt", "youtube.com"),
|
||||||
|
Map.entry("bra", "search.brave.com"),
|
||||||
|
Map.entry("bs", "search.brave.com"),
|
||||||
|
Map.entry("gh", "github.com"),
|
||||||
|
Map.entry("google", "google.com"),
|
||||||
|
Map.entry("duck", "duckduckgo.com"),
|
||||||
|
Map.entry("bing", "bing.com"),
|
||||||
|
Map.entry("meta", "metager.de"),
|
||||||
|
Map.entry("mojeek", "mojeek.com"),
|
||||||
|
Map.entry("searx", "searx.be"),
|
||||||
|
Map.entry("startpage", "startpage.com"),
|
||||||
|
Map.entry("stack", "stackoverflow.com"),
|
||||||
|
Map.entry("so", "stackoverflow.com"),
|
||||||
|
Map.entry("oracle", "oracle.com"),
|
||||||
|
Map.entry("docs", "docs.oracle.com"),
|
||||||
|
Map.entry("mdn", "developer.mozilla.org"),
|
||||||
|
Map.entry("maven", "maven.apache.org"),
|
||||||
|
Map.entry("gradle", "gradle.org"),
|
||||||
|
Map.entry("spring", "spring.io"),
|
||||||
|
Map.entry("jetbrains", "jetbrains.com"),
|
||||||
|
Map.entry("uni", "unibas.ch")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves user input into a valid URL.
|
||||||
|
*
|
||||||
|
* @param input User input from the URL field, which can be a full URL, a domain name, or an alias.
|
||||||
|
* @return A properly formatted URL string that can be loaded by the browser, or the original input
|
||||||
|
* if it cannot be resolved.
|
||||||
|
*/
|
||||||
|
private static String resolveInputToUrl(String input) {
|
||||||
|
if (input == null || input.isBlank()) return input;
|
||||||
|
|
||||||
|
input = input.trim();
|
||||||
|
|
||||||
|
String alias = ALIASES.get(input.toLowerCase());
|
||||||
|
if (alias != null) {
|
||||||
|
return "https://" + alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://" + input;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean javascriptEnabled = false;
|
private static boolean javascriptEnabled = false;
|
||||||
|
|
||||||
/** Deletes all cookies stored during the current browser session. */
|
/** Deletes all cookies stored during the current browser session. */
|
||||||
@@ -203,9 +269,9 @@ public class CasinoBrowserController {
|
|||||||
engine.setCreatePopupHandler(
|
engine.setCreatePopupHandler(
|
||||||
config -> {
|
config -> {
|
||||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||||
alert.setTitle("Popup blockiert");
|
alert.setTitle("Popup blocked");
|
||||||
alert.setHeaderText(null);
|
alert.setHeaderText(null);
|
||||||
alert.setContentText("Popup wurde aus Sicherheitsgründen blockiert.");
|
alert.setContentText("The Casono browser has detected a threat and successfully blocked it.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||||
@@ -283,6 +349,64 @@ public class CasinoBrowserController {
|
|||||||
urlField.getStyleClass().add("gray-input-field");
|
urlField.getStyleClass().add("gray-input-field");
|
||||||
HBox.setHgrow(urlField, Priority.ALWAYS);
|
HBox.setHgrow(urlField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
urlField.textProperty().addListener((obs, oldText, newText) -> {
|
||||||
|
if (newText == null || newText.isBlank()) {
|
||||||
|
autoCompleteMenu.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String input = newText.toLowerCase();
|
||||||
|
|
||||||
|
String aliasMatch = ALIASES.get(input);
|
||||||
|
if (aliasMatch != null) {
|
||||||
|
autoCompleteMenu.getItems().clear();
|
||||||
|
|
||||||
|
MenuItem item = new MenuItem(aliasMatch);
|
||||||
|
item.setOnAction(e -> {
|
||||||
|
urlField.setText("https://" + aliasMatch);
|
||||||
|
autoCompleteMenu.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCompleteMenu.getItems().add(item);
|
||||||
|
autoCompleteMenu.show(urlField, javafx.geometry.Side.BOTTOM, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoCompleteMenu.getItems().clear();
|
||||||
|
|
||||||
|
URL_SUGGESTIONS.stream()
|
||||||
|
.filter(domain -> domain.toLowerCase().startsWith(input)
|
||||||
|
|| domain.toLowerCase().contains(input))
|
||||||
|
.sorted((a, b) -> {
|
||||||
|
boolean aStarts = a.startsWith(input);
|
||||||
|
boolean bStarts = b.startsWith(input);
|
||||||
|
return Boolean.compare(!aStarts, !bStarts);
|
||||||
|
})
|
||||||
|
.limit(5)
|
||||||
|
.forEach(domain -> {
|
||||||
|
MenuItem item = new MenuItem(domain);
|
||||||
|
|
||||||
|
item.setOnAction(e -> {
|
||||||
|
urlField.setText("https://" + domain);
|
||||||
|
autoCompleteMenu.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCompleteMenu.getItems().add(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!autoCompleteMenu.getItems().isEmpty()) {
|
||||||
|
autoCompleteMenu.show(urlField,
|
||||||
|
javafx.geometry.Side.BOTTOM,
|
||||||
|
0, 0);
|
||||||
|
} else {
|
||||||
|
autoCompleteMenu.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
urlField.focusedProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (!newVal) autoCompleteMenu.hide();
|
||||||
|
});
|
||||||
|
|
||||||
return urlField;
|
return urlField;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +429,7 @@ public class CasinoBrowserController {
|
|||||||
* @return Toggle button
|
* @return Toggle button
|
||||||
*/
|
*/
|
||||||
public static Button createJsToggle(WebEngine engine) {
|
public static Button createJsToggle(WebEngine engine) {
|
||||||
Button jsToggle = new Button("JS EINSCHALTEN");
|
Button jsToggle = new Button("JS TURN ON");
|
||||||
jsToggle.getStyleClass().add("red-button");
|
jsToggle.getStyleClass().add("red-button");
|
||||||
|
|
||||||
jsToggle.setOnAction(
|
jsToggle.setOnAction(
|
||||||
@@ -319,7 +443,7 @@ public class CasinoBrowserController {
|
|||||||
jsToggle.getStyleClass().add("yellow-button");
|
jsToggle.getStyleClass().add("yellow-button");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
jsToggle.setText("JS EINSCHALTEN");
|
jsToggle.setText("JS TURN ON");
|
||||||
jsToggle.getStyleClass().removeAll("yellow-button");
|
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||||
jsToggle.getStyleClass().add("red-button");
|
jsToggle.getStyleClass().add("red-button");
|
||||||
}
|
}
|
||||||
@@ -410,7 +534,15 @@ public class CasinoBrowserController {
|
|||||||
*/
|
*/
|
||||||
public static void configureUrlEvents(
|
public static void configureUrlEvents(
|
||||||
WebEngine engine, TextField urlField, Label securityLabel) {
|
WebEngine engine, TextField urlField, Label securityLabel) {
|
||||||
urlField.setOnAction(e -> loadUrlSafely(engine, urlField.getText(), securityLabel));
|
urlField.setOnKeyPressed(e -> {
|
||||||
|
if (e.getCode() == KeyCode.ENTER) {
|
||||||
|
if (!autoCompleteMenu.isShowing()) {
|
||||||
|
String resolved = resolveInputToUrl(urlField.getText());
|
||||||
|
urlField.setText(resolved);
|
||||||
|
loadUrlSafely(engine, resolved, securityLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
engine.locationProperty().addListener((obs, o, n) -> urlField.setText(n));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,27 +600,45 @@ public class CasinoBrowserController {
|
|||||||
*/
|
*/
|
||||||
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||||
try {
|
try {
|
||||||
if (!url.startsWith("http")) {
|
if (!url.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||||
url = "https://" + url;
|
url = "https://" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
URI uri = new URI(url);
|
URI uri = new URI(url);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
|
||||||
// HTTPS required
|
if ("file".equalsIgnoreCase(scheme)) {
|
||||||
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
Path allowed = Paths.get(
|
||||||
|
System.getProperty("user.dir"),
|
||||||
|
"documents",
|
||||||
|
"docs",
|
||||||
|
"game-engine",
|
||||||
|
"manual.html"
|
||||||
|
).normalize();
|
||||||
|
|
||||||
|
Path requested = Paths.get(uri).normalize();
|
||||||
|
|
||||||
|
if (requested.equals(allowed)) {
|
||||||
|
securityLabel.setText("LOCAL OK");
|
||||||
|
engine.load(uri.toString());
|
||||||
|
} else {
|
||||||
|
securityLabel.setText("BLOCKED");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"https".equalsIgnoreCase(scheme)) {
|
||||||
securityLabel.setText("BLOCKED");
|
securityLabel.setText("BLOCKED");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String host = uri.getHost();
|
String host = uri.getHost();
|
||||||
if (host == null) {
|
if (host == null || host.isBlank()) {
|
||||||
securityLabel.setText("ERROR");
|
securityLabel.setText("ERROR");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secure Domain Check
|
boolean trusted = TRUSTED_DOMAINS.stream()
|
||||||
boolean trusted =
|
|
||||||
TRUSTED_DOMAINS.stream()
|
|
||||||
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||||
|
|
||||||
if (!trusted) {
|
if (!trusted) {
|
||||||
@@ -500,9 +650,11 @@ public class CasinoBrowserController {
|
|||||||
} else {
|
} else {
|
||||||
securityLabel.setText("SAFE");
|
securityLabel.setText("SAFE");
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.load(uri.toString());
|
engine.load(uri.toString());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
securityLabel.setText("ERROR");
|
securityLabel.setText("INVALID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +671,7 @@ public class CasinoBrowserController {
|
|||||||
alert.setHeaderText("This website is unknown");
|
alert.setHeaderText("This website is unknown");
|
||||||
String content =
|
String content =
|
||||||
host
|
host
|
||||||
+ "\n\nThis site has not been verified by the Casono browser.\n"
|
+ "\n\nThis site has not been verified by the Casono Browser.\n"
|
||||||
+ "Do you still want to open it?";
|
+ "Do you still want to open it?";
|
||||||
alert.setContentText(content);
|
alert.setContentText(content);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user