Add: secure local HTML file loading in Casono Browser and website autocompletion
This commit is contained in:
+168
-16
@@ -4,7 +4,10 @@ import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javafx.application.Platform;
|
||||
@@ -29,6 +32,10 @@ import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
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.
|
||||
@@ -63,6 +70,11 @@ public class CasinoBrowserController {
|
||||
|
||||
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 CORNER_RADIUS = 40;
|
||||
private static final int HBOX_SPACIN = 10;
|
||||
@@ -79,7 +91,9 @@ public class CasinoBrowserController {
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
TRUSTED_DOMAINS.add("wikipedia.org");
|
||||
TRUSTED_DOMAINS.add("youtube.com");
|
||||
TRUSTED_DOMAINS.add("github.com");
|
||||
TRUSTED_DOMAINS.add("search.brave.com");
|
||||
TRUSTED_DOMAINS.add("oracle.com");
|
||||
TRUSTED_DOMAINS.add("stackoverflow.com");
|
||||
TRUSTED_DOMAINS.add("docs.oracle.com");
|
||||
@@ -94,10 +108,62 @@ public class CasinoBrowserController {
|
||||
TRUSTED_DOMAINS.add("metager.de");
|
||||
TRUSTED_DOMAINS.add("unibas.ch");
|
||||
TRUSTED_DOMAINS.add("mojeek.com");
|
||||
TRUSTED_DOMAINS.add("searx.be ");
|
||||
TRUSTED_DOMAINS.add("searx.be");
|
||||
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;
|
||||
|
||||
/** Deletes all cookies stored during the current browser session. */
|
||||
@@ -203,9 +269,9 @@ public class CasinoBrowserController {
|
||||
engine.setCreatePopupHandler(
|
||||
config -> {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Popup blockiert");
|
||||
alert.setTitle("Popup blocked");
|
||||
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 {
|
||||
var stream = CasinoBrowserController.class.getResourceAsStream(LOGO_PATH);
|
||||
@@ -283,6 +349,64 @@ public class CasinoBrowserController {
|
||||
urlField.getStyleClass().add("gray-input-field");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -305,7 +429,7 @@ public class CasinoBrowserController {
|
||||
* @return Toggle button
|
||||
*/
|
||||
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.setOnAction(
|
||||
@@ -319,7 +443,7 @@ public class CasinoBrowserController {
|
||||
jsToggle.getStyleClass().add("yellow-button");
|
||||
|
||||
} else {
|
||||
jsToggle.setText("JS EINSCHALTEN");
|
||||
jsToggle.setText("JS TURN ON");
|
||||
jsToggle.getStyleClass().removeAll("yellow-button");
|
||||
jsToggle.getStyleClass().add("red-button");
|
||||
}
|
||||
@@ -410,7 +534,15 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
public static void configureUrlEvents(
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -468,28 +600,46 @@ public class CasinoBrowserController {
|
||||
*/
|
||||
private static void loadUrlSafely(WebEngine engine, String url, Label securityLabel) {
|
||||
try {
|
||||
if (!url.startsWith("http")) {
|
||||
if (!url.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
|
||||
URI uri = new URI(url);
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
// HTTPS required
|
||||
if (!"https".equalsIgnoreCase(uri.getScheme())) {
|
||||
if ("file".equalsIgnoreCase(scheme)) {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null) {
|
||||
if (host == null || host.isBlank()) {
|
||||
securityLabel.setText("ERROR");
|
||||
return;
|
||||
}
|
||||
|
||||
// Secure Domain Check
|
||||
boolean trusted =
|
||||
TRUSTED_DOMAINS.stream()
|
||||
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||
boolean trusted = TRUSTED_DOMAINS.stream()
|
||||
.anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain));
|
||||
|
||||
if (!trusted) {
|
||||
if (!showUnknownWebsiteAlert(host)) {
|
||||
@@ -500,9 +650,11 @@ public class CasinoBrowserController {
|
||||
} else {
|
||||
securityLabel.setText("SAFE");
|
||||
}
|
||||
|
||||
engine.load(uri.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
securityLabel.setText("ERROR");
|
||||
securityLabel.setText("INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +671,7 @@ public class CasinoBrowserController {
|
||||
alert.setHeaderText("This website is unknown");
|
||||
String content =
|
||||
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?";
|
||||
alert.setContentText(content);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user