This commit is contained in:
2025-05-10 07:48:08 -04:00
parent 44294205c8
commit ecb801b7d5

View File

@@ -11,12 +11,17 @@ import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake; import org.java_websocket.handshake.ServerHandshake;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -24,12 +29,26 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
private NostrWebSocketClient webSocketClient; private NostrWebSocketClient webSocketClient;
private final Set<Player> authenticatedPlayers = new HashSet<>(); private final Set<Player> authenticatedPlayers = new HashSet<>();
private final Map<UUID, PendingAuth> pendingAuths = new ConcurrentHashMap<>();
// Class to hold pending authentication data
private static class PendingAuth {
final Player player;
final String pubkey;
String minecraftUsername;
boolean metadataVerified = false;
PendingAuth(Player player, String pubkey) {
this.player = player;
this.pubkey = pubkey;
}
}
@Override @Override
public void onEnable() { public void onEnable() {
getLogger().info("BukkitstrPlugin enabled!"); getLogger().info("BukkitstrPlugin enabled!");
try { try {
URI uri = new URI("wss://"); // Replace with your Nostr relay URL URI uri = new URI("ws://192.168.1.14:8888"); // Replace with your Nostr relay URL
webSocketClient = new NostrWebSocketClient(uri, getLogger()); webSocketClient = new NostrWebSocketClient(uri, getLogger());
webSocketClient.connect(); webSocketClient.connect();
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
@@ -48,23 +67,139 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (sender instanceof Player) { if (!(sender instanceof Player)) {
Player player = (Player) sender; sender.sendMessage("This command can only be used by players");
player.sendMessage("Waiting for Nostr event...");
webSocketClient.waitForEvent(content -> handleSpecificEvent(content, player));
return true; return true;
} }
Player player = (Player) sender;
if (command.getName().equalsIgnoreCase("nostrauth")) {
if (args.length < 1) {
player.sendMessage("Usage: /nostrauth <pubkey>");
return true;
}
String pubkey = args[0];
player.sendMessage("Starting Nostr authentication for pubkey: " + pubkey);
// Create a unique ID for this authentication attempt
UUID authId = UUID.randomUUID();
String metadataSubId = "meta_" + authId.toString().replace("-", "");
String authSubId = "auth_" + authId.toString().replace("-", "");
// Store the pending authentication
PendingAuth pendingAuth = new PendingAuth(player, pubkey);
pendingAuths.put(authId, pendingAuth);
// First step: Check for metadata (kind 30078)
player.sendMessage("§6Checking your Nostr profile...");
webSocketClient.subscribeToParameterizedReplaceable(metadataSubId, 30078, pubkey, "mcauth", authId,
(success, username) -> {
if (success && username != null) {
pendingAuth.metadataVerified = true;
pendingAuth.minecraftUsername = username;
// Verify username matches
if (username.equalsIgnoreCase(player.getName())) {
player.sendMessage("§6Profile verified! Waiting for authentication event...");
// Now subscribe to auth events (kind 13378008)
webSocketClient.subscribeToAuthEvents(authSubId, 13378008, pubkey, authId);
} else {
player.sendMessage("§cAuthentication failed: The Minecraft username in your Nostr profile (" +
username + ") doesn't match your in-game name (" + player.getName() + ")");
webSocketClient.closeSubscription(metadataSubId);
pendingAuths.remove(authId);
}
} else {
player.sendMessage("§cCouldn't find a Nostr profile with the Minecraft username. " +
"Make sure you've created a kind 30078 event with d=mcauth containing your username.");
webSocketClient.closeSubscription(metadataSubId);
pendingAuths.remove(authId);
}
}
);
return true;
}
return false; return false;
} }
private void handleSpecificEvent(String content, Player player) { // Process an authentication event (kind 13378008)
if (content.equals(player.getName())) { private void handleAuthEvent(UUID authId, JsonNode eventNode) {
authenticatedPlayers.add(player); PendingAuth auth = pendingAuths.get(authId);
player.sendMessage("You are now authenticated."); if (auth == null || !auth.metadataVerified) {
} else { getLogger().warning("Received auth event for unknown auth ID or unverified metadata: " + authId);
player.sendMessage("Authentication failed. Username does not match."); return;
}
Player player = auth.player;
try {
// Get event kind to verify it's 13378008
int kind = eventNode.path("kind").asInt();
if (kind != 13378008) {
getLogger().warning("Expected kind 13378008, got: " + kind);
return;
}
// Verify the pubkey matches
String eventPubkey = eventNode.path("pubkey").asText("");
if (!eventPubkey.equals(auth.pubkey)) {
player.sendMessage("§cAuthentication failed: Event signed by wrong pubkey");
return;
}
// Check tags for the d tag with "mcauth" value
boolean hasMcAuthTag = false;
JsonNode tagsNode = eventNode.path("tags");
if (tagsNode.isArray()) {
for (JsonNode tag : tagsNode) {
if (tag.isArray() && tag.size() > 1 &&
"d".equals(tag.get(0).asText()) &&
"mcauth".equals(tag.get(1).asText())) {
hasMcAuthTag = true;
break;
}
}
}
if (!hasMcAuthTag) {
player.sendMessage("§cAuthentication failed: Event is missing the d=mcauth tag");
getLogger().warning("Missing d=mcauth tag in auth event");
return;
}
// Get the content
String content = eventNode.path("content").asText("");
// Log the received event details
getLogger().info("Auth event received - Content: '" + content + "', Player: '" + player.getName() + "'");
// Verify content matches the player's name
if (content.equalsIgnoreCase(player.getName())) {
// Authentication successful!
authenticatedPlayers.add(player);
player.sendMessage("§a✓ Authentication successful! You can now move freely.");
player.sendMessage("§6 Please note, you'll need to reauth ~30 days");
getLogger().info("Player " + player.getName() + " authenticated successfully with pubkey " + auth.pubkey);
// Cleanup
String authSubId = "auth_" + authId.toString().replace("-", "");
String metaSubId = "meta_" + authId.toString().replace("-", "");
webSocketClient.closeSubscription(authSubId);
webSocketClient.closeSubscription(metaSubId);
pendingAuths.remove(authId);
} else {
player.sendMessage("§cAuthentication failed: Event content doesn't match your username");
getLogger().warning("Auth event content '" + content + "' doesn't match player name '" + player.getName() + "'");
}
} catch (Exception e) {
getLogger().severe("Error processing auth event: " + e.getMessage());
player.sendMessage("§cAn error occurred during authentication. Please try again.");
} }
} }
@@ -73,7 +208,7 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
Player player = event.getPlayer(); Player player = event.getPlayer();
if (!authenticatedPlayers.contains(player)) { if (!authenticatedPlayers.contains(player)) {
event.setCancelled(true); event.setCancelled(true);
player.sendMessage("You need to authenticate first!"); player.sendMessage("§cYou need to authenticate first with /nostrauth <pubkey>");
} }
} }
@@ -81,7 +216,7 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
private final Logger logger; private final Logger logger;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private Consumer<String> eventConsumer; private final Map<String, UUID> subscriptionMap = new HashMap<>();
public NostrWebSocketClient(URI serverUri, Logger logger) { public NostrWebSocketClient(URI serverUri, Logger logger) {
super(serverUri); super(serverUri);
@@ -92,7 +227,6 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
@Override @Override
public void onOpen(ServerHandshake handshakedata) { public void onOpen(ServerHandshake handshakedata) {
logger.info("Connected to Nostr relay: " + getURI()); logger.info("Connected to Nostr relay: " + getURI());
sendInitialRequest();
} }
@Override @Override
@@ -114,37 +248,217 @@ public class BukkitstrPlugin extends JavaPlugin implements Listener {
private void handleNostrEvent(String message) { private void handleNostrEvent(String message) {
try { try {
JsonNode jsonNode = objectMapper.readTree(message); JsonNode jsonNode = objectMapper.readTree(message);
if (jsonNode.isArray() && jsonNode.size() > 2) {
// Log all messages for debugging
logger.info("Processing Nostr message: " + message);
// Check if this is an EVENT message
if (jsonNode.isArray() && jsonNode.size() >= 3 && "EVENT".equals(jsonNode.get(0).asText())) {
String subscriptionId = jsonNode.get(1).asText();
JsonNode eventNode = jsonNode.get(2); JsonNode eventNode = jsonNode.get(2);
JsonNode tagsNode = eventNode.path("tags");
if (tagsNode.isArray()) { // Log the event kind for debugging
for (JsonNode tag : tagsNode) { int kind = eventNode.path("kind").asInt();
if (tag.isArray() && tag.size() > 1 && "d".equals(tag.get(0).asText())) { logger.info("Received event with kind: " + kind + " for subscription: " + subscriptionId);
String content = tag.get(1).asText();
if (eventConsumer != null) { // Figure out what kind of subscription this is
eventConsumer.accept(content); if (subscriptionId.startsWith("meta_")) {
} logger.info("Processing as metadata event");
handleMetadataResponse(subscriptionId, eventNode);
} else if (subscriptionId.startsWith("auth_")) {
logger.info("Processing as auth event");
handleAuthResponse(subscriptionId, eventNode);
}
} else if (jsonNode.isArray() && jsonNode.size() >= 3 && "EOSE".equals(jsonNode.get(0).asText())) {
// End of stored events
String subscriptionId = jsonNode.get(1).asText();
logger.info("Received EOSE for subscription: " + subscriptionId);
if (subscriptionId.startsWith("meta_")) {
UUID authId = getAuthIdFromSubscription(subscriptionId);
if (authId != null) {
PendingAuth auth = pendingAuths.get(authId);
if (auth != null && !auth.metadataVerified) {
// No metadata found by EOSE
auth.player.sendMessage("§cNo Nostr profile found with Minecraft authentication. " +
"Please set up your profile first.");
closeSubscription(subscriptionId);
pendingAuths.remove(authId);
} }
} }
} else {
logger.warning("No 'tags' array found in the event message.");
} }
} else {
logger.warning("Event message is not in the expected format.");
} }
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
logger.severe("Failed to parse Nostr event: " + e.getMessage()); logger.severe("Failed to parse Nostr event: " + e.getMessage());
} }
} }
public void waitForEvent(Consumer<String> eventConsumer) { private void handleMetadataResponse(String subscriptionId, JsonNode eventNode) {
this.eventConsumer = eventConsumer; UUID authId = getAuthIdFromSubscription(subscriptionId);
if (authId == null) return;
try {
// Check if this is kind 30078
int kind = eventNode.path("kind").asInt();
if (kind != 30078) return;
// Look for d tag with "mcauth" value
String dValue = null;
JsonNode tagsNode = eventNode.path("tags");
if (tagsNode.isArray()) {
for (JsonNode tag : tagsNode) {
if (tag.isArray() && tag.size() > 1 &&
"d".equals(tag.get(0).asText())) {
dValue = tag.get(1).asText();
break;
}
}
}
if (!"mcauth".equals(dValue)) {
logger.info("Skipping 30078 event with d=" + dValue + " (looking for d=mcauth)");
return;
}
// Parse content for the Minecraft username
String content = eventNode.path("content").asText("");
String username = content.trim();
if (username.isEmpty()) {
logger.warning("Empty Minecraft username in 30078 event");
return;
}
PendingAuth auth = pendingAuths.get(authId);
if (auth != null) {
// Callback with success
auth.player.sendMessage("§6Found Nostr profile with Minecraft username: " + username);
auth.metadataVerified = true;
auth.minecraftUsername = username;
// Check if username matches
if (username.equalsIgnoreCase(auth.player.getName())) {
auth.player.sendMessage("§6Username verified! Waiting for authentication event...");
// Now subscribe to auth events (kind 13378008)
String authSubId = "auth_" + authId.toString().replace("-", "");
subscribeToAuthEvents(authSubId, 13378008, auth.pubkey, authId);
} else {
auth.player.sendMessage("§cAuthentication failed: The Minecraft username in your Nostr profile (" +
username + ") doesn't match your in-game name (" + auth.player.getName() + ")");
closeSubscription(subscriptionId);
pendingAuths.remove(authId);
}
}
} catch (Exception e) {
logger.severe("Error handling metadata response: " + e.getMessage());
}
} }
private void sendInitialRequest() { private void handleAuthResponse(String subscriptionId, JsonNode eventNode) {
String initialRequest = "[\"REQ\",\"mc_auth\",{\"kinds\":[13378008],\"limit\":0}]"; UUID authId = getAuthIdFromSubscription(subscriptionId);
send(initialRequest); if (authId != null) {
logger.info("Sent initial request to Nostr relay: " + initialRequest); // Log the event details for debugging
int kind = eventNode.path("kind").asInt();
String pubkey = eventNode.path("pubkey").asText("");
String content = eventNode.path("content").asText("");
logger.info("Auth event received - Kind: " + kind + ", Pubkey: " + pubkey + ", Content: '" + content + "'");
// Only process kind 13378008 events for authentication
if (kind == 13378008) {
logger.info("Processing kind 13378008 event for authentication");
handleAuthEvent(authId, eventNode);
} else {
logger.info("Ignoring event with kind " + kind + " (expecting 13378008)");
}
} else {
logger.warning("Cannot find auth ID for subscription: " + subscriptionId);
}
}
private UUID getAuthIdFromSubscription(String subscriptionId) {
// Extract authId from subscription ID
String idPart = subscriptionId.substring(subscriptionId.indexOf('_') + 1);
try {
return UUID.fromString(
idPart.replaceFirst(
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
"$1-$2-$3-$4-$5"
)
);
} catch (IllegalArgumentException e) {
logger.warning("Invalid UUID in subscription ID: " + idPart);
return null;
}
}
// Subscribe to parameterized replaceable events (for metadata)
public void subscribeToParameterizedReplaceable(String subscriptionId, int kind, String pubkey,
String dValue, UUID authId,
MetadataCallback callback) {
try {
// Create proper filter for Nostr - using array notation for kinds and correct #d tag format
String filterJson = "{" +
"\"kinds\":[" + kind + "]," +
"\"authors\":[\"" + pubkey + "\"]," +
"\"#d\":[\"" + dValue + "\"]" +
"}";
JsonNode filter = objectMapper.readTree(filterJson);
String reqMessage = "[\"REQ\",\"" + subscriptionId + "\"," + filter.toString() + "]";
send(reqMessage);
logger.info("Sent metadata subscription: " + reqMessage);
// Store the subscription ID mapping
subscriptionMap.put(subscriptionId, authId);
} catch (Exception e) {
logger.severe("Failed to create metadata subscription: " + e.getMessage());
}
}
// Subscribe to authentication events
public void subscribeToAuthEvents(String subscriptionId, int kind, String pubkey, UUID authId) {
try {
// Create proper filter format for Nostr - using array notation for #d tag
String filterJson = "{" +
"\"kinds\":[" + kind + "]," +
"\"authors\":[\"" + pubkey + "\"]," +
"\"#d\":[\"mcauth\"]" +
"}";
JsonNode filter = objectMapper.readTree(filterJson);
String reqMessage = "[\"REQ\",\"" + subscriptionId + "\"," + filter.toString() + "]";
send(reqMessage);
logger.info("Sent auth subscription: " + reqMessage);
// Store the subscription ID mapping
subscriptionMap.put(subscriptionId, authId);
// Notify player
PendingAuth auth = pendingAuths.get(authId);
if (auth != null) {
auth.player.sendMessage("§6Waiting for authentication event (kind 13378008)...");
auth.player.sendMessage("§6Please sign an event with your Nostr client.");
}
} catch (Exception e) {
logger.severe("Failed to create auth subscription: " + e.getMessage());
}
}
public void closeSubscription(String subscriptionId) {
String closeMessage = "[\"CLOSE\",\"" + subscriptionId + "\"]";
send(closeMessage);
logger.info("Closed subscription: " + closeMessage);
subscriptionMap.remove(subscriptionId);
} }
} }
// Interface for metadata callback
interface MetadataCallback {
void onResult(boolean success, String username);
}
} }