Compare commits

...

6 Commits
1.0 ... 1.1

Author SHA1 Message Date
games647
59703bac4e Fix race condition in BungeeCord 2016-05-13 18:54:08 +02:00
games647
bfaf390463 Fixed bungeecord detection for older Spigot builds 2016-05-12 20:11:56 +02:00
games647
9e06fd7735 Added support for the configuration options under BungeeCord 2016-05-06 08:55:09 +02:00
games647
d56a0f9ff1 Fix thread-safety in async forcelogin task 2016-05-05 12:00:22 +02:00
games647
96fe190cac Ignore module target folders from git too 2016-05-05 09:55:01 +02:00
games647
d4f5b547d4 Listen to the success of the bukkit module 2016-05-05 09:46:21 +02:00
23 changed files with 1678 additions and 1528 deletions

7
.gitignore vendored
View File

@@ -40,4 +40,9 @@ hs_err_pid*
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
!gradle-wrapper.jar
# Project module targets
bukkit/target
universal/target
bungee/target

View File

@@ -1,3 +1,9 @@
######1.1
* Make the configuration options also work under BungeeCord (premiumUUID, forwardSkin)
* Catch configuration loading exception if it's not spigot build
* Fix config loading for older Spigot builds
######1.0
* Massive refactor to handle errors on force actions safely

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.0</version>
<version>1.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -3,6 +3,7 @@ package com.github.games647.fastlogin.bukkit;
import com.comphenix.protocol.AsynchronousManager;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.utility.SafeCacheBuilder;
import com.github.games647.fastlogin.bukkit.commands.CrackedCommand;
import com.github.games647.fastlogin.bukkit.commands.PremiumCommand;
@@ -16,6 +17,7 @@ import com.google.common.cache.CacheLoader;
import com.google.common.reflect.ClassPath;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.KeyPair;
import java.sql.SQLException;
import java.util.UUID;
@@ -25,6 +27,7 @@ import java.util.logging.Level;
import org.apache.commons.lang.RandomStringUtils;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
@@ -32,7 +35,7 @@ import org.bukkit.plugin.java.JavaPlugin;
* This plugin checks if a player has a paid account and if so tries to skip offline mode authentication.
*/
public class FastLoginBukkit extends JavaPlugin {
private static final int WORKER_THREADS = 5;
public static UUID parseId(String withoutDashes) {
@@ -46,7 +49,6 @@ public class FastLoginBukkit extends JavaPlugin {
//provide a immutable key pair to be thread safe | used for encrypting and decrypting traffic
private final KeyPair keyPair = EncryptionUtil.generateKeyPair();
private boolean bungeeCord;
private Storage storage;
@@ -79,7 +81,20 @@ public class FastLoginBukkit extends JavaPlugin {
return;
}
bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord");
try {
if (Bukkit.spigot().getConfig().isBoolean("settings.bungeecord")) {
bungeeCord = Bukkit.spigot().getConfig().getBoolean("settings.bungeecord");
} else {
Method getConfigMethod = FuzzyReflection.fromObject(getServer().spigot(), true)
.getMethodByName("getSpigotConfig");
getConfigMethod.setAccessible(true);
YamlConfiguration spigotConfig = (YamlConfiguration) getConfigMethod.invoke(getServer().spigot());
bungeeCord = spigotConfig.getBoolean("settings.bungeecord");
}
} catch (Exception | NoSuchMethodError ex) {
getLogger().warning("Cannot check bungeecord support. You use a non-spigot build");
}
boolean hookFound = registerHooks();
if (bungeeCord) {
getLogger().info("BungeeCord setting detected. No auth plugin is required");

View File

@@ -1,17 +1,22 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.metadata.FixedMetadataValue;
public class ForceLoginTask implements Runnable {
protected final FastLoginBukkit plugin;
private final Player player;
protected final Player player;
public ForceLoginTask(FastLoginBukkit plugin, Player player) {
this.plugin = plugin;
@@ -20,49 +25,57 @@ public class ForceLoginTask implements Runnable {
@Override
public void run() {
if (!player.isOnline()) {
if (!isOnlineThreadSafe()) {
return;
}
//remove the bungeecord identifier
//remove the bungeecord identifier if there is ones
String id = '/' + player.getAddress().getAddress().getHostAddress() + ':' + player.getAddress().getPort();
PlayerSession session = plugin.getSessions().get(id);
//blacklist this target player for BungeeCord Id brute force attacks
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
//check if it's the same player as we checked before
final BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
if (session == null || !player.getName().equals(session.getUsername()) || authPlugin == null) {
return;
}
BukkitAuthPlugin authPlugin = plugin.getAuthPlugin();
Storage storage = plugin.getStorage();
PlayerProfile playerProfile = null;
if (storage != null) {
playerProfile = storage.getProfile(session.getUsername(), false);
playerProfile = storage.getProfile(player.getName(), false);
}
if (session.isVerified()) {
boolean success = true;
if (session == null) {
//cracked player
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
playerProfile.setPremium(true);
playerProfile.setUuid(null);
playerProfile.setPremium(false);
storage.save(playerProfile);
}
if (success) {
if (session.needsRegistration()) {
if (forceRegister(authPlugin, player)) {
storage.save(playerProfile);
}
} else {
if (forceLogin(authPlugin, player)) {
storage.save(playerProfile);
//check if it's the same player as we checked before
} else if (player.getName().equals(session.getUsername())) {
//premium player
if (authPlugin == null) {
//maybe only bungeecord plugin
sendSuccessNotification();
} else {
boolean success = false;
if (isOnlineThreadSafe() && session.isVerified()) {
if (session.needsRegistration()) {
success = forceRegister(authPlugin, player);
} else {
success = forceLogin(authPlugin, player);
}
}
if (success) {
//update only on success to prevent corrupt data
if (playerProfile != null) {
playerProfile.setUuid(session.getUuid());
//save cracked players too
playerProfile.setPremium(session.isVerified());
storage.save(playerProfile);
}
sendSuccessNotification();
}
}
} else if (playerProfile != null) {
storage.save(playerProfile);
}
}
@@ -82,4 +95,30 @@ public class ForceLoginTask implements Runnable {
player.sendMessage(ChatColor.DARK_GREEN + "Auto logged in");
return success;
}
private void sendSuccessNotification() {
if (plugin.isBungeeCord()) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
dataOutput.writeUTF("SUCCESS");
player.sendPluginMessage(plugin, plugin.getName(), dataOutput.toByteArray());
}
}
private boolean isOnlineThreadSafe() {
//the playerlist isn't thread-safe
Future<Boolean> onlineFuture = Bukkit.getScheduler().callSyncMethod(plugin, new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return player.isOnline();
}
});
try {
return onlineFuture.get();
} catch (InterruptedException | ExecutionException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to perform thread-safe online check", ex);
return false;
}
}
}

View File

@@ -68,7 +68,7 @@ public class PlayerProfile {
this.lastIp = lastIp;
}
public long getLastLogin() {
public synchronized long getLastLogin() {
return lastLogin;
}

View File

@@ -54,7 +54,7 @@ public class PremiumCommand implements CommandExecutor {
plugin.getStorage().save(profile);
}
});
sender.sendMessage(ChatColor.DARK_GREEN + "Added to the list of premium players");
}
}

View File

@@ -68,7 +68,7 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
try {
LoginPlayerData result = future.get();
if (result != null) {
if (result != null && result.isLoggedIn()) {
//SQL-Queries should run async
crazyLoginPlugin.getCrazyDatabase().saveWithoutPassword(result);
return true;
@@ -88,7 +88,7 @@ public class CrazyLoginHook implements BukkitAuthPlugin {
@Override
public boolean forceRegister(final Player player, String password) {
final CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
CrazyLoginDataDatabase crazyDatabase = crazyLoginPlugin.getCrazyDatabase();
//this executes a sql query and accesses only thread safe collections so we can run it async
LoginPlayerData playerData = crazyLoginPlugin.getPlayerData(player.getName());

File diff suppressed because it is too large Load Diff

View File

@@ -81,6 +81,12 @@ public class LoginSecurityHook implements BukkitAuthPlugin {
//this executes a sql query without interacting with other parts so we can run it async.
dataManager.register(uuidString, passwordHash, securityPlugin.hasher.getTypeId(), ipAddress.toString());
return forceLogin(player);
String storedPassword = dataManager.getPassword(uuidString);
if (storedPassword != null && storedPassword.equals(passwordHash)) {
//the register method silents any excpetion so check if our entry was saved
return forceLogin(player);
}
return false;
}
}

View File

@@ -34,7 +34,7 @@ public class RoyalAuthHook implements BukkitAuthPlugin {
//not thread-safe
authPlayer.login();
return true;
return authPlayer.isLoggedIn();
}
});

View File

@@ -32,11 +32,10 @@ public class xAuthHook implements BukkitAuthPlugin {
xAuthPlayer xAuthPlayer = xAuthPlugin.getPlayerManager().getPlayer(player);
if (xAuthPlayer != null) {
//we checked that the player is premium (paid account)
//unprotect the inventory, op status...
xAuthPlayer.setPremium(true);
xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
return true;
//unprotect the inventory, op status...
return xAuthPlugin.getPlayerManager().doLogin(xAuthPlayer);
}
return false;

View File

@@ -19,7 +19,7 @@ import org.bukkit.event.player.PlayerQuitEvent;
*/
public class BukkitJoinListener implements Listener {
private static final long DELAY_LOGIN = 1 * 20L / 2;
private static final long DELAY_LOGIN = 20L / 2;
protected final FastLoginBukkit plugin;
@@ -41,8 +41,10 @@ public class BukkitJoinListener implements Listener {
}
}
//Wait before auth plugin and we received a message from BungeeCord initializes the player
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin, player), DELAY_LOGIN);
if (!plugin.isBungeeCord()) {
//Wait before auth plugin and we received a message from BungeeCord initializes the player
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, new ForceLoginTask(plugin, player), DELAY_LOGIN);
}
}
@EventHandler

View File

@@ -1,6 +1,7 @@
package com.github.games647.fastlogin.bukkit.listener;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.ForceLoginTask;
import com.github.games647.fastlogin.bukkit.PlayerSession;
import com.github.games647.fastlogin.bukkit.hooks.BukkitAuthPlugin;
import com.google.common.base.Charsets;
@@ -54,17 +55,24 @@ public class BungeeCordListener implements PluginMessageListener {
final Player checkedPlayer = plugin.getServer().getPlayerExact(playerName);
//fail if target player is blacklisted because already authed or wrong bungeecord id
if (checkedPlayer != null && !checkedPlayer.hasMetadata(plugin.getName())) {
//blacklist this target player for BungeeCord Id brute force attacks
player.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
//bungeecord UUID
long mostSignificantBits = dataInput.readLong();
long leastSignificantBits = dataInput.readLong();
UUID sourceId = new UUID(mostSignificantBits, leastSignificantBits);
plugin.getLogger().log(Level.FINEST, "Received proxy id {0} from {1}", new Object[]{sourceId, player});
//fail if BungeeCord support is disabled (id = null)
if (sourceId.equals(proxyId)) {
final PlayerSession playerSession = new PlayerSession(playerName);
final String id = '/' + checkedPlayer.getAddress().getAddress().getHostAddress() + ':'
+ checkedPlayer.getAddress().getPort();
if ("AUTO_LOGIN".equalsIgnoreCase(subchannel)) {
playerSession.setVerified(true);
playerSession.setRegistered(true);
plugin.getSessions().put(checkedPlayer.getAddress().toString(), playerSession);
plugin.getSessions().put(id, playerSession);
} else if ("AUTO_REGISTER".equalsIgnoreCase(subchannel)) {
playerSession.setVerified(true);
@@ -75,7 +83,7 @@ public class BungeeCordListener implements PluginMessageListener {
try {
//we need to check if the player is registered on Bukkit too
if (authPlugin != null && !authPlugin.isRegistered(playerName)) {
plugin.getSessions().put(checkedPlayer.getAddress().toString(), playerSession);
plugin.getSessions().put(id, playerSession);
}
} catch (Exception ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to query isRegistered", ex);
@@ -83,9 +91,8 @@ public class BungeeCordListener implements PluginMessageListener {
}
});
}
} else {
//blacklist target for the current login
checkedPlayer.setMetadata(plugin.getName(), new FixedMetadataValue(plugin, true));
Bukkit.getScheduler().runTaskAsynchronously(plugin, new ForceLoginTask(plugin, player));
}
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.0</version>
<version>1.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,8 +13,9 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.Util;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
@@ -25,17 +26,13 @@ import net.md_5.bungee.config.YamlConfiguration;
*/
public class FastLoginBungee extends Plugin {
public static UUID parseId(String withoutDashes) {
return UUID.fromString(withoutDashes.substring(0, 8)
+ "-" + withoutDashes.substring(8, 12)
+ "-" + withoutDashes.substring(12, 16)
+ "-" + withoutDashes.substring(16, 20)
+ "-" + withoutDashes.substring(20, 32));
}
private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
public static UUID parseId(String withoutDashes) {
return Util.getUUID(withoutDashes);
}
private BungeeAuthPlugin bungeeAuthPlugin;
private final MojangApiConnector mojangApiConnector = new MojangApiConnector(this);
private Storage storage;

View File

@@ -13,60 +13,62 @@ public class ForceLoginTask implements Runnable {
private final FastLoginBungee plugin;
private final ProxiedPlayer player;
private final Server server;
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player) {
public ForceLoginTask(FastLoginBungee plugin, ProxiedPlayer player, Server server) {
this.plugin = plugin;
this.player = player;
this.server = server;
}
@Override
public void run() {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
if (playerProfile.getUserId() == -1) {
playerProfile.setPremium(player.getPendingConnection().isOnlineMode());
if (player.getPendingConnection().isOnlineMode()) {
playerProfile.setUuid(player.getUniqueId());
}
}
//force login only on success
if (player.getPendingConnection().isOnlineMode()) {
Server server = player.getServer();
boolean autoRegister = plugin.getPendingAutoRegister().remove(player.getPendingConnection()) != null;
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
//subchannel name
if (autoRegister) {
dataOutput.writeUTF("AUTO_REGISTER");
} else {
dataOutput.writeUTF("AUTO_LOGIN");
}
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
dataOutput.writeUTF(player.getName());
//proxy identifier to check if it's a acceptable proxy
UUID proxyId = UUID.fromString(plugin.getProxy().getConfig().getUuid());
dataOutput.writeLong(proxyId.getMostSignificantBits());
dataOutput.writeLong(proxyId.getLeastSignificantBits());
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
BungeeAuthPlugin authPlugin = plugin.getBungeeAuthPlugin();
if (authPlugin != null) {
if (authPlugin == null) {
sendBukkitLoginNotification(autoRegister);
} else if (player.isConnected()) {
if (autoRegister) {
String password = plugin.generateStringPassword();
if (authPlugin.forceRegister(player, password)) {
plugin.getStorage().save(playerProfile);
sendBukkitLoginNotification(autoRegister);
}
} else if (authPlugin.forceLogin(player)) {
plugin.getStorage().save(playerProfile);
sendBukkitLoginNotification(autoRegister);
}
}
} else {
//cracked player
//update only on success to prevent corrupt data
playerProfile.setPremium(false);
plugin.getStorage().save(playerProfile);
}
}
private void sendBukkitLoginNotification(boolean autoRegister) {
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
//subchannel name
if (autoRegister) {
dataOutput.writeUTF("AUTO_REGISTER");
} else {
dataOutput.writeUTF("AUTO_LOGIN");
}
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
dataOutput.writeUTF(player.getName());
//proxy identifier to check if it's a acceptable proxy
UUID proxyId = UUID.fromString(plugin.getProxy().getConfig().getUuid());
dataOutput.writeLong(proxyId.getMostSignificantBits());
dataOutput.writeLong(proxyId.getLeastSignificantBits());
if (server != null) {
server.sendData(plugin.getDescription().getName(), dataOutput.toByteArray());
}
}
}

View File

@@ -1,6 +1,7 @@
package com.github.games647.fastlogin.bungee;
import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -9,6 +10,7 @@ import java.net.URL;
import java.util.UUID;
import java.util.logging.Level;
import java.util.regex.Pattern;
import net.md_5.bungee.BungeeCord;
public class MojangApiConnector {

View File

@@ -1,8 +1,10 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.bungee.hooks.BungeeAuthPlugin;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Level;
@@ -14,9 +16,13 @@ import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.connection.LoginResult.Property;
import net.md_5.bungee.event.EventHandler;
/**
@@ -74,10 +80,47 @@ public class PlayerConnectionListener implements Listener {
});
}
@EventHandler
public void onLogin(PostLoginEvent loginEvent) {
ProxiedPlayer player = loginEvent.getPlayer();
PendingConnection connection = player.getPendingConnection();
String username = connection.getName();
if (connection.isOnlineMode()) {
PlayerProfile playerProfile = plugin.getStorage().getProfile(player.getName(), false);
playerProfile.setUuid(player.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
InitialHandler initialHandler = (InitialHandler) connection;
if (!plugin.getConfiguration().getBoolean("premiumUuid")) {
try {
UUID offlineUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(Charsets.UTF_8));
Field idField = initialHandler.getClass().getDeclaredField("uniqueId");
idField.setAccessible(true);
idField.set(connection, offlineUUID);
//bungeecord doesn't support overriding the premium uuid
// connection.setUniqueId(offlineUUID);
} catch (NoSuchFieldException | IllegalAccessException ex) {
plugin.getLogger().log(Level.SEVERE, "Failed to set offline uuid", ex);
}
}
if (!plugin.getConfiguration().getBoolean("forwardSkin")) {
//this is null on offline mode
LoginResult loginProfile = initialHandler.getLoginProfile();
if (loginProfile != null) {
loginProfile.setProperties(new Property[]{});
}
}
}
}
@EventHandler
public void onServerConnected(ServerConnectedEvent serverConnectedEvent) {
ProxiedPlayer player = serverConnectedEvent.getPlayer();
ProxyServer.getInstance().getScheduler().runAsync(plugin, new ForceLoginTask(plugin, player));
ForceLoginTask loginTask = new ForceLoginTask(plugin, player, serverConnectedEvent.getServer());
ProxyServer.getInstance().getScheduler().runAsync(plugin, loginTask);
}
@EventHandler
@@ -137,8 +180,24 @@ public class PlayerConnectionListener implements Listener {
playerProfile.setUuid(null);
//todo: set uuid
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Added to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
});
} else if ("SUCCESS".equals(subchannel)) {
if (forPlayer.getPendingConnection().isOnlineMode()) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
PlayerProfile playerProfile = plugin.getStorage().getProfile(forPlayer.getName(), false);
playerProfile.setPremium(true);
//we override this in the loginevent
// playerProfile.setUuid(forPlayer.getUniqueId());
plugin.getStorage().save(playerProfile);
TextComponent textComponent = new TextComponent("Removed to the list of premium players");
textComponent.setColor(ChatColor.DARK_GREEN);
forPlayer.sendMessage(textComponent);
}
}
}
}

View File

@@ -68,7 +68,7 @@ public class PlayerProfile {
this.lastIp = lastIp;
}
public long getLastLogin() {
public synchronized long getLastLogin() {
return lastLogin;
}

View File

@@ -8,7 +8,7 @@
<packaging>pom</packaging>
<name>FastLogin</name>
<version>1.0</version>
<version>1.1</version>
<inceptionYear>2015</inceptionYear>
<url>https://www.spigotmc.org/resources/fastlogin.14153/</url>
<description>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>com.github.games647</groupId>
<artifactId>fastlogin</artifactId>
<version>1.0</version>
<version>1.1</version>
<relativePath>../pom.xml</relativePath>
</parent>