Skip to content

Commit

Permalink
Add an origin checker to warn when World Host was downloaded from an …
Browse files Browse the repository at this point in the history
…unofficial source

The warning only appears once per installation of World Host
  • Loading branch information
Gaming32 committed Sep 23, 2024
1 parent 48a367a commit 6e890ec
Show file tree
Hide file tree
Showing 12 changed files with 684 additions and 5 deletions.
25 changes: 22 additions & 3 deletions src/main/java/io/github/gaming32/worldhost/WorldHost.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.github.gaming32.worldhost.gui.screen.FriendsScreen;
import io.github.gaming32.worldhost.gui.screen.JoiningWorldHostScreen;
import io.github.gaming32.worldhost.gui.screen.OnlineFriendsScreen;
import io.github.gaming32.worldhost.origincheck.OriginCheckers;
import io.github.gaming32.worldhost.plugin.FriendAdder;
import io.github.gaming32.worldhost.plugin.InfoTextsCategory;
import io.github.gaming32.worldhost.plugin.OnlineFriend;
Expand Down Expand Up @@ -239,7 +240,10 @@ public class WorldHost
@Override
public void onInitializeClient() {
final var container = FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow();
init(path -> container.findPath(path).orElseThrow(() -> new NoSuchFileException(path)));
init(
path -> container.findPath(path).orElseThrow(() -> new NoSuchFileException(path)),
container.getOrigin().getPaths().getFirst()
);
}
//#else
//$$ public WorldHost(
Expand All @@ -251,7 +255,7 @@ public void onInitializeClient() {
//$$ final ModContainer container = ModLoadingContext.get().getActiveContainer();
//#endif
//$$ final var modFile = container.getModInfo().getOwningFile().getFile();
//$$ init(path -> modFile.findResource(path.split("/")));
//$$ init(path -> modFile.findResource(path.split("/")), modFile.getFilePath());
//$$ container.registerExtensionPoint(
//#if MC >= 1.20.5
//$$ IConfigScreenFactory.class, (ignored, screen) -> new WorldHostConfigScreen(screen)
Expand All @@ -266,7 +270,7 @@ public void onInitializeClient() {
//$$ }
//#endif

private static void init(IOFunction<String, Path> assetGetter) {
private static void init(IOFunction<String, Path> assetGetter, Path modPath) {
try (BufferedReader reader = Files.newBufferedReader(
assetGetter.apply("16k.txt"), StandardCharsets.US_ASCII
)) {
Expand All @@ -288,6 +292,21 @@ private static void init(IOFunction<String, Path> assetGetter) {
loadConfig();
prepareFileWatcher();

final var nonstandardOrigins = OriginCheckers.getNonstandardOriginsOnce(OriginCheckers.NATIVE_CHECKER, modPath);
if (!nonstandardOrigins.isEmpty()) {
LOGGER.warn("Found nonstandard download origins: {}", nonstandardOrigins);
WHToast.builder("world-host.nonstandard_origin")
.description(Components.translatable(
"world-host.nonstandard_origin.desc",
nonstandardOrigins.stream()
.map(URI::getHost)
.collect(Collectors.joining(", "))
))
.important()
.ticks(200)
.show();
}

try {
Files.createDirectories(CACHE_DIR);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.gaming32.worldhost.origincheck;

import java.net.URI;
import java.nio.file.Path;
import java.util.List;

public interface OriginChecker {
boolean needsChecking(Path file);

List<URI> findOrigins(Path file);

void markChecked(Path file);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.github.gaming32.worldhost.origincheck;

import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.origincheck.checkers.GioOriginChecker;
import io.github.gaming32.worldhost.origincheck.checkers.MacOriginChecker;
import io.github.gaming32.worldhost.origincheck.checkers.NoopOriginChecker;
import io.github.gaming32.worldhost.origincheck.checkers.WindowsOriginChecker;
import net.minecraft.Util;

import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Predicate;

public class OriginCheckers {
public static final OriginChecker NATIVE_CHECKER = switch (Util.getPlatform()) {
case WINDOWS -> new WindowsOriginChecker();
case OSX -> new MacOriginChecker();
default -> {
try {
yield new GioOriginChecker();
} catch (IllegalStateException e) {
WorldHost.LOGGER.info("GIO origin checker not available", e);
yield new NoopOriginChecker();
}
}
};
public static final List<String> STANDARD_ORIGINS = List.of(
"modrinth.com",
"github.com",
"githubusercontent.com",
"maven.jemnetworks.com"
);

public static boolean isStandardHost(String host) {
final var hostLength = host.length();
for (final var origin : STANDARD_ORIGINS) {
if (host.equals(origin)) {
return true;
}
final var originLength = origin.length();
if (hostLength <= originLength) continue;
if (host.endsWith(origin) && host.charAt(hostLength - originLength - 1) == '.') {
return true;
}
}
return false;
}

public static boolean hasStandardOrigin(URI uri) {
return isStandardHost(uri.getHost());
}

public static List<URI> getNonstandardOrigins(List<URI> uris) {
return uris.stream().filter(Predicate.not(OriginCheckers::hasStandardOrigin)).toList();
}

public static List<URI> getNonstandardOrigins(OriginChecker checker, Path file) {
return getNonstandardOrigins(checker.findOrigins(file));
}

public static List<URI> getNonstandardOriginsOnce(OriginChecker checker, Path file) {
if (!checker.needsChecking(file)) {
return List.of();
}
final var result = getNonstandardOrigins(checker, file);
checker.markChecked(file);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.github.gaming32.worldhost.origincheck.checkers;

import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.origincheck.OriginChecker;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.util.ArrayList;
import java.util.List;

abstract class AbstractOriginChecker implements OriginChecker {
abstract String getCheckedMarker();

abstract String getOriginAttributeName();

abstract List<@Nullable String> parseOrigins(ByteBuffer bb) throws Exception;

@Override
public boolean needsChecking(Path file) {
final var attributes = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class);
if (attributes == null) {
return false;
}
try {
return !attributes.list().contains(getCheckedMarker());
} catch (IOException e) {
WorldHost.LOGGER.error("Failed to check for {} on {}", getCheckedMarker(), file, e);
return false;
}
}

@Override
public List<URI> findOrigins(Path file) {
final var attributes = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class);
if (attributes == null) {
return List.of();
}
try {
if (!attributes.list().contains(getOriginAttributeName())) {
return List.of();
}
final var bb = ByteBuffer.allocate(attributes.size(getOriginAttributeName()));
attributes.read(getOriginAttributeName(), bb);
bb.flip();
return toUriList(parseOrigins(bb));
} catch (Exception e) {
WorldHost.LOGGER.error("Failed to read {} on {}", getOriginAttributeName(), file, e);
return List.of();
}
}

private List<URI> toUriList(List<@Nullable String> uris) {
final var result = new ArrayList<URI>(uris.size());
for (final var url : uris) {
if (url != null) {
try {
result.add(new URI(url));
} catch (URISyntaxException e) {
WorldHost.LOGGER.warn("Failed to parse {} URL {}", getOriginAttributeName(), url, e);
}
}
}
return result;
}

@Override
public void markChecked(Path file) {
final var attributes = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class);
if (attributes == null) return;
try {
attributes.write(getCheckedMarker(), ByteBuffer.allocate(0));
} catch (IOException e) {
WorldHost.LOGGER.error("Failed to write {} on {}", getCheckedMarker(), file, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.github.gaming32.worldhost.origincheck.checkers;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import io.github.gaming32.worldhost.WorldHost;
import io.github.gaming32.worldhost.origincheck.OriginChecker;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

// This is more spotty, as not all web browsers implement this (for example, Firefox does, but Chromium doesn't).
// Furthermore, most things won't copy the metadata with the file, which means this metadata often won't be available
// from the mods directory, unless the file was directly saved there and not copied. When I wrote this, I only knew
// about the browser limitation, not the copying limitation, otherwise I wouldn't have written it.
public class GioOriginChecker implements OriginChecker {
private static final String CHECKED_MARKER = "metadata::world-host-origin-checked";
private static final String DOWNLOAD_URI = "metadata::download-uri";

private final Gio gio;

public GioOriginChecker() throws IllegalStateException {
try {
gio = Native.load("gio-2.0", Gio.class);
} catch (UnsatisfiedLinkError e) {
throw new IllegalStateException("Gio library not available", e);
}
}

@Override
public boolean needsChecking(Path file) {
return doWithInfo(
file, CHECKED_MARKER, false,
(gioFile, info) -> gio.g_file_info_get_attribute_string(info, CHECKED_MARKER) == null
);
}

@Override
public List<URI> findOrigins(Path file) {
final var uri = doWithInfo(
file, DOWNLOAD_URI, null,
(gioFile, info) -> gio.g_file_info_get_attribute_string(info, DOWNLOAD_URI)
);
if (uri == null) {
return List.of();
}
try {
return List.of(new URI(uri));
} catch (URISyntaxException e) {
WorldHost.LOGGER.warn("Failed to parse {} URI {}", DOWNLOAD_URI, uri, e);
return List.of();
}
}

@Override
public void markChecked(Path file) {
doWithInfo(file, "", (gioFile, info) -> {
gio.g_file_info_set_attribute_string(info, CHECKED_MARKER, "");
if (!gio.g_file_set_attributes_from_info(gioFile, info, 0, null, null)) {
WorldHost.LOGGER.warn("Failed to set {} on {}", CHECKED_MARKER, file);
}
});
}

private void doWithInfo(Path file, String attributes, BiConsumer<Pointer, Pointer> action) {
doWithInfo(file, attributes, null, (gioFile, info) -> {
action.accept(gioFile, info);
return null;
});
}

private <T> T doWithInfo(Path file, String attributes, T defaultValue, BiFunction<Pointer, Pointer, T> action) {
if (file.getFileSystem() != FileSystems.getDefault()) {
return defaultValue;
}
final var gioFile = gio.g_file_new_for_path(file.toString());
try {
final var info = gio.g_file_query_info(gioFile, attributes, 0, null, null);
if (info == null) {
return defaultValue;
}
try {
return action.apply(gioFile, info);
} finally {
Native.free(Pointer.nativeValue(info));
}
} finally {
Native.free(Pointer.nativeValue(gioFile));
}
}

private interface Gio extends Library {
Pointer g_file_new_for_path(String path);

Pointer g_file_query_info(Pointer file, String attributes, int flags, Pointer cancellable, Pointer error);

String g_file_info_get_attribute_string(Pointer info, String attribute);

void g_file_info_set_attribute_string(Pointer info, String attribute, String attr_value);

boolean g_file_set_attributes_from_info(Pointer file, Pointer info, int flags, Pointer cancellable, Pointer error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.gaming32.worldhost.origincheck.checkers;

import io.github.gaming32.worldhost.origincheck.parser.SimpleBplistParser;

import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.List;

public class MacOriginChecker extends AbstractOriginChecker {
@Override
String getCheckedMarker() {
return "io.github.gaming32.worldhost:originChecked";
}

@Override
String getOriginAttributeName() {
return "com.apple.metadata:kMDItemWhereFroms";
}

@Override
List<String> parseOrigins(ByteBuffer bb) {
final var bplist = SimpleBplistParser.parseBplist(bb);
if (!(bplist instanceof Collection<?> collection)) {
return List.of();
}
return collection.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.github.gaming32.worldhost.origincheck.checkers;

import io.github.gaming32.worldhost.origincheck.OriginChecker;

import java.net.URI;
import java.nio.file.Path;
import java.util.List;

public class NoopOriginChecker implements OriginChecker {
@Override
public boolean needsChecking(Path file) {
return false;
}

@Override
public List<URI> findOrigins(Path file) {
return List.of();
}

@Override
public void markChecked(Path file) {
}
}
Loading

0 comments on commit 6e890ec

Please sign in to comment.