new updater
This commit is contained in:
		
					parent
					
						
							
								68bbd9b9a5
							
						
					
				
			
			
				commit
				
					
						1fab21601e
					
				
			
		
					 13 changed files with 156 additions and 537 deletions
				
			
		
							
								
								
									
										5
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								pom.xml
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -51,6 +51,11 @@
 | 
			
		|||
		    <artifactId>wtapi</artifactId>
 | 
			
		||||
		    <version>0.7</version>
 | 
			
		||||
		</dependency>
 | 
			
		||||
		<dependency>
 | 
			
		||||
			<groupId>eu.m724</groupId>
 | 
			
		||||
		    <artifactId>jarupdater</artifactId>
 | 
			
		||||
		    <version>0.1.0</version>
 | 
			
		||||
		</dependency>
 | 
			
		||||
	</dependencies>
 | 
			
		||||
	
 | 
			
		||||
    <build>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,8 +21,7 @@ import eu.m724.realweather.thunder.ThunderConfig;
 | 
			
		|||
import eu.m724.realweather.thunder.ThunderMaster;
 | 
			
		||||
import eu.m724.realweather.time.TimeConfig;
 | 
			
		||||
import eu.m724.realweather.time.TimeMaster;
 | 
			
		||||
import eu.m724.realweather.updater.SignatureValidator;
 | 
			
		||||
import eu.m724.realweather.updater.Updater;
 | 
			
		||||
import eu.m724.realweather.updater.PluginUpdater;
 | 
			
		||||
import eu.m724.realweather.updater.UpdaterConfig;
 | 
			
		||||
import eu.m724.realweather.weather.PlayerWeatherDirectory;
 | 
			
		||||
import eu.m724.realweather.weather.WeatherConfig;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +33,7 @@ public class RealWeatherPlugin extends JavaPlugin {
 | 
			
		|||
	private WeatherMaster weatherMaster;
 | 
			
		||||
	private ThunderMaster thunderMaster;
 | 
			
		||||
	private TimeMaster timeMaster;
 | 
			
		||||
	private Updater updater;
 | 
			
		||||
	private PluginUpdater updater;
 | 
			
		||||
	
 | 
			
		||||
	private Logger logger;
 | 
			
		||||
	
 | 
			
		||||
| 
						 | 
				
			
			@ -42,17 +41,6 @@ public class RealWeatherPlugin extends JavaPlugin {
 | 
			
		|||
	public void onEnable() {
 | 
			
		||||
		logger = getLogger();
 | 
			
		||||
		
 | 
			
		||||
		 // TODO remove these lines
 | 
			
		||||
		SignatureValidator signatureValidator = new SignatureValidator(this);
 | 
			
		||||
		logger.info("Signature of this JAR: " + signatureValidator.getCertificate().getSubjectX500Principal().getName());
 | 
			
		||||
		
 | 
			
		||||
		if (!signatureValidator.isValid()) {
 | 
			
		||||
			logger.severe("Key is not valid");
 | 
			
		||||
			getServer().getPluginManager().disablePlugin(this);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		 // TODO remove those lines
 | 
			
		||||
		
 | 
			
		||||
		File dataFolder = getDataFolder();
 | 
			
		||||
		File modulesFolder = new File("modules");
 | 
			
		||||
		modulesFolder.mkdir();
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +107,7 @@ public class RealWeatherPlugin extends JavaPlugin {
 | 
			
		|||
			timeMaster.init();
 | 
			
		||||
			
 | 
			
		||||
			GlobalConstants.updaterConfig = UpdaterConfig.fromConfiguration(configuration.getConfigurationSection("updater"));
 | 
			
		||||
			updater = new Updater(GlobalConstants.updaterConfig);
 | 
			
		||||
			updater = PluginUpdater.build(this, GlobalConstants.updaterConfig);
 | 
			
		||||
			updater.init();
 | 
			
		||||
		} catch (UserError | NoSuchProviderException e) {
 | 
			
		||||
			logger.severe("There are errors in your config:");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@ import eu.m724.realweather.mapper.MapperConfig;
 | 
			
		|||
import eu.m724.realweather.thunder.ThunderConfig;
 | 
			
		||||
import eu.m724.realweather.thunder.ThunderMaster;
 | 
			
		||||
import eu.m724.realweather.time.TimeConfig;
 | 
			
		||||
import eu.m724.realweather.updater.Updater;
 | 
			
		||||
import eu.m724.realweather.updater.PluginUpdater;
 | 
			
		||||
import eu.m724.realweather.weather.WeatherConfig;
 | 
			
		||||
import net.md_5.bungee.api.ChatColor;
 | 
			
		||||
import net.md_5.bungee.api.chat.BaseComponent;
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ public class AdminCommand implements CommandExecutor {
 | 
			
		|||
	private BaseComponent enabledComponent = TextComponent.fromLegacy("YES\n", ChatColor.GREEN);
 | 
			
		||||
	private BaseComponent disabledComponent = TextComponent.fromLegacy("NO\n", ChatColor.RED);
 | 
			
		||||
	
 | 
			
		||||
	public AdminCommand(Updater updater) {
 | 
			
		||||
	public AdminCommand(PluginUpdater updater) {
 | 
			
		||||
		this.updateCommand = new UpdateCommand(updater);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,15 @@
 | 
			
		|||
package eu.m724.realweather.commands;
 | 
			
		||||
 | 
			
		||||
import java.nio.file.NoSuchFileException;
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.command.Command;
 | 
			
		||||
import org.bukkit.command.CommandSender;
 | 
			
		||||
 | 
			
		||||
import eu.m724.realweather.updater.Updater;
 | 
			
		||||
import eu.m724.realweather.updater.metadata.VersionMetadata;
 | 
			
		||||
import eu.m724.jarupdater.Updater;
 | 
			
		||||
import eu.m724.jarupdater.object.Version;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * not actually a command but deserves a separate file
 | 
			
		||||
| 
						 | 
				
			
			@ -22,13 +25,13 @@ public class UpdateCommand {
 | 
			
		|||
		if (!sender.hasPermission("realweather.admin.update")) return false;
 | 
			
		||||
		
 | 
			
		||||
		sender.sendMessage("Please wait");
 | 
			
		||||
		CompletableFuture<VersionMetadata> latestFuture = updater.getLatestVersion();
 | 
			
		||||
		CompletableFuture<Version> latestFuture = updater.getLatestVersion();
 | 
			
		||||
		
 | 
			
		||||
		if (args.length == 0) {
 | 
			
		||||
			latestFuture.thenAccept(metadata -> {
 | 
			
		||||
				if (metadata != null) {
 | 
			
		||||
					sender.sendMessage("An update is available!");
 | 
			
		||||
					sender.sendMessage("RealWeather %s released %s".formatted(metadata.label, metadata.getFormattedDate()));
 | 
			
		||||
					sender.sendMessage("RealWeather %s released %s".formatted(metadata.getLabel(), formatDate(metadata.getTimestamp())));
 | 
			
		||||
					sender.sendMessage("To download: /rwadmin update download");
 | 
			
		||||
				} else {
 | 
			
		||||
					sender.sendMessage("No new updates"); // TODO color
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +43,7 @@ public class UpdateCommand {
 | 
			
		|||
			if (action.equals("download")) {
 | 
			
		||||
				sender.sendMessage("Started download");
 | 
			
		||||
				
 | 
			
		||||
				updater.downloadUpdate().handle((file, ex) -> {
 | 
			
		||||
				updater.downloadLatestVersion().handle((file, ex) -> {
 | 
			
		||||
					sender.sendMessage("Download failed. See console for details.");
 | 
			
		||||
					ex.printStackTrace();
 | 
			
		||||
					return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,14 +53,25 @@ public class UpdateCommand {
 | 
			
		|||
				});
 | 
			
		||||
				
 | 
			
		||||
			} else if (action.equals("install")) {
 | 
			
		||||
				if (updater.installUpdate())
 | 
			
		||||
					sender.sendMessage("Update installed, restart server to apply.");
 | 
			
		||||
				else sender.sendMessage("Update not installed, see console.");
 | 
			
		||||
				try {
 | 
			
		||||
					updater.installLatestVersion().handle((v, ex) -> {
 | 
			
		||||
						sender.sendMessage("Install failed. See console for details.");
 | 
			
		||||
						ex.printStackTrace();
 | 
			
		||||
						return null;
 | 
			
		||||
					}).thenAccept(v -> {
 | 
			
		||||
						sender.sendMessage("Installation completed, restart server to apply");
 | 
			
		||||
					});
 | 
			
		||||
				} catch (NoSuchFileException e) {
 | 
			
		||||
					sender.sendMessage("Download the update first");
 | 
			
		||||
				}
 | 
			
		||||
			} else return false;
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private String formatDate(long timestamp) { // TODO move this
 | 
			
		||||
		return DateTimeFormatter.ofPattern("dd.MM.yyyy").format(Instant.ofEpochSecond(timestamp));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.plugin.Plugin;
 | 
			
		||||
 | 
			
		||||
import eu.m724.jarupdater.environment.ConstantEnvironment;
 | 
			
		||||
import eu.m724.realweather.GlobalConstants;
 | 
			
		||||
 | 
			
		||||
public class PluginEnvironment extends ConstantEnvironment {
 | 
			
		||||
 | 
			
		||||
	public PluginEnvironment(Plugin plugin) {
 | 
			
		||||
		super(plugin.getDescription().getVersion(),
 | 
			
		||||
				GlobalConstants.getUpdaterConfig().channel,
 | 
			
		||||
				Path.of(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/main/java/eu/m724/realweather/updater/PluginUpdater.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/main/java/eu/m724/realweather/updater/PluginUpdater.java
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.plugin.Plugin;
 | 
			
		||||
 | 
			
		||||
import eu.m724.jarupdater.Updater;
 | 
			
		||||
import eu.m724.jarupdater.download.Downloader;
 | 
			
		||||
import eu.m724.jarupdater.download.SimpleDownloader;
 | 
			
		||||
import eu.m724.jarupdater.environment.Environment;
 | 
			
		||||
import eu.m724.jarupdater.live.GiteaMetadataDAO;
 | 
			
		||||
import eu.m724.jarupdater.live.MetadataDAO;
 | 
			
		||||
import eu.m724.jarupdater.live.MetadataFacade;
 | 
			
		||||
 | 
			
		||||
public class PluginUpdater extends Updater {
 | 
			
		||||
	private UpdaterConfig updaterConfig;
 | 
			
		||||
	
 | 
			
		||||
	PluginUpdater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, UpdaterConfig updaterConfig) {
 | 
			
		||||
		super(environment, metadataProvider, downloader);
 | 
			
		||||
		this.updaterConfig = updaterConfig;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	
 | 
			
		||||
	public static PluginUpdater build(Plugin plugin, UpdaterConfig updaterConfig) {
 | 
			
		||||
		Environment environment = new PluginEnvironment(plugin);
 | 
			
		||||
		MetadataDAO metadataDAO = new GiteaMetadataDAO("https://git.m724.eu/Minecon724/realweather", "master");
 | 
			
		||||
		MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
 | 
			
		||||
		Downloader downloader = new SimpleDownloader("realweather");
 | 
			
		||||
		
 | 
			
		||||
		return new PluginUpdater(environment, metadataFacade, downloader, updaterConfig);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void init() {
 | 
			
		||||
		if (!updaterConfig.notify) return;
 | 
			
		||||
		
 | 
			
		||||
		UpdateNotifier updateNotifier = new UpdateNotifier(this, (version) -> {});
 | 
			
		||||
		updateNotifier.register();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,40 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import java.security.KeyFactory;
 | 
			
		||||
import java.security.NoSuchAlgorithmException;
 | 
			
		||||
import java.security.PublicKey;
 | 
			
		||||
import java.security.cert.X509Certificate;
 | 
			
		||||
import java.security.spec.InvalidKeySpecException;
 | 
			
		||||
import java.security.spec.X509EncodedKeySpec;
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
 | 
			
		||||
import eu.m724.realweather.RealWeatherPlugin;
 | 
			
		||||
 | 
			
		||||
// TODO rework this for updater
 | 
			
		||||
public class SignatureValidator {
 | 
			
		||||
	public RealWeatherPlugin plugin;
 | 
			
		||||
	public static final String encodedPublicKey = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAptv/9qIJXrs/4T1MOkY1QPU/TuLsyCsJdoA2PIO1qdS3nRJBPgkRf2sK6nG1VhOaUHLXoj8lZtQQLcY76CNLqFGFmimo7RDnJjHpxHUzI9pKZJOQ9sEVCDFtoLQiit23t6MAO7GBjJXMNFLonxyay6pTABJo3VYyjg2bE4kd1wjg73RPMQY+zykaRQBUE167PAVkmuYxJK680EYmZph9kQTS12autU2qGFTvsPbmmdhtF7Xy8u84CtEucgRT9HSh0y8MuC0esMGhZtB9gsWcGET763DHtArEMekBnjByb3k+gGiG0Y1K9ygBn+nNVKP66KJGCWFuno8xy+LNiZKX4pUnrJcTyLvZg7PvjdZTye54PKkAAOACAbcFBiat38Zes5ZOKZIBEjC2IXbhfySoOn5WAk+XPsm3gVlgO9d51iOVDDBx5MCqq802lOyIGog1BlbhnGZ2+cSvFo7ZWpF0f93uG5UKBqRF+Q9cPA36SMUAoQ2DWFEZOYXwFgCXxVvFAgMBAAE=";
 | 
			
		||||
	
 | 
			
		||||
	public SignatureValidator(RealWeatherPlugin plugin) {
 | 
			
		||||
		this.plugin = plugin;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public X509Certificate getCertificate() {
 | 
			
		||||
		return (X509Certificate) plugin.getClass().getProtectionDomain().getCodeSource().getCertificates()[0];
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public boolean isValid() {
 | 
			
		||||
		PublicKey currentPublicKey = getCertificate().getPublicKey();
 | 
			
		||||
		PublicKey expectedPublicKey = null;
 | 
			
		||||
		
 | 
			
		||||
		try {
 | 
			
		||||
			X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(encodedPublicKey));
 | 
			
		||||
			expectedPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
 | 
			
		||||
		} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
 | 
			
		||||
			// TODO Auto-generated catch block
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		return expectedPublicKey.equals(currentPublicKey);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.FileInputStream;
 | 
			
		||||
import java.io.FileOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.net.ProxySelector;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.http.HttpClient;
 | 
			
		||||
import java.net.http.HttpRequest;
 | 
			
		||||
import java.net.http.HttpResponse;
 | 
			
		||||
import java.net.http.HttpClient.Redirect;
 | 
			
		||||
import java.net.http.HttpResponse.BodyHandlers;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.security.MessageDigest;
 | 
			
		||||
import java.security.NoSuchAlgorithmException;
 | 
			
		||||
import java.security.SignatureException;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.HexFormat;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.concurrent.CompletionException;
 | 
			
		||||
 | 
			
		||||
public class UpdateDownloader {
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * 
 | 
			
		||||
	 * @param url
 | 
			
		||||
	 * @param sha256hex
 | 
			
		||||
	 * @return
 | 
			
		||||
	 * @throws SignatureException if signature doesnt match
 | 
			
		||||
	 */
 | 
			
		||||
	public CompletableFuture<File> downloadAndVerify(String url, String sha256hex) {
 | 
			
		||||
		return download(url).thenApply(file -> {
 | 
			
		||||
			try {
 | 
			
		||||
				byte[] hash = computeFileSha256Hash(file);
 | 
			
		||||
				boolean matches = compareBytesHex(hash, sha256hex);
 | 
			
		||||
				
 | 
			
		||||
				if (!matches) {
 | 
			
		||||
					// TODO clean?
 | 
			
		||||
					throw new SignatureException();
 | 
			
		||||
				}
 | 
			
		||||
				
 | 
			
		||||
				return file;
 | 
			
		||||
			} catch (IOException | SignatureException e) {
 | 
			
		||||
				throw new CompletionException(e);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private CompletableFuture<File> download(String url) { // TODO progress?
 | 
			
		||||
		
 | 
			
		||||
		HttpRequest request = HttpRequest.newBuilder()
 | 
			
		||||
				.uri(URI.create(url))
 | 
			
		||||
				.header("User-Agent", "rwu/1") // real weather updater v1
 | 
			
		||||
				.build();
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<HttpResponse<InputStream>> responseFuture =
 | 
			
		||||
				HttpClient.newBuilder()
 | 
			
		||||
				.followRedirects(Redirect.NORMAL)
 | 
			
		||||
				.proxy(ProxySelector.getDefault()).build().
 | 
			
		||||
				sendAsync(request, BodyHandlers.ofInputStream());
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<File> fileFuture =
 | 
			
		||||
				responseFuture.thenApply(response -> {
 | 
			
		||||
					File downloadFile = null;
 | 
			
		||||
					try {
 | 
			
		||||
						InputStream bodyStream = response.body();
 | 
			
		||||
						downloadFile = Files.createTempFile("realweather", null).toFile();
 | 
			
		||||
						FileOutputStream fileStream = new FileOutputStream(downloadFile);
 | 
			
		||||
			
 | 
			
		||||
						while (bodyStream.available() > 0) {
 | 
			
		||||
							bodyStream.transferTo(fileStream);
 | 
			
		||||
						}
 | 
			
		||||
						
 | 
			
		||||
						bodyStream.close();
 | 
			
		||||
					} catch (IOException e) {
 | 
			
		||||
						throw new CompletionException(e);
 | 
			
		||||
					}
 | 
			
		||||
					
 | 
			
		||||
					return downloadFile;
 | 
			
		||||
				});
 | 
			
		||||
		
 | 
			
		||||
		return fileFuture;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private byte[] computeFileSha256Hash(File file) throws IOException {
 | 
			
		||||
		MessageDigest digest = null;
 | 
			
		||||
		try {
 | 
			
		||||
			digest = MessageDigest.getInstance("SHA-256");
 | 
			
		||||
		} catch (NoSuchAlgorithmException e) {
 | 
			
		||||
			throw new RuntimeException(e);
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		try (FileInputStream fileInputStream = new FileInputStream(file)) {
 | 
			
		||||
			byte[] buffer = new byte[16384];
 | 
			
		||||
			int len;
 | 
			
		||||
			
 | 
			
		||||
			while ((len = fileInputStream.read(buffer)) != -1)
 | 
			
		||||
				digest.update(buffer, 0, len);
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		return digest.digest();
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private boolean compareBytesHex(byte[] bytes, String hecks) {
 | 
			
		||||
		return Arrays.equals(bytes, HexFormat.of().parseHex(hecks));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.concurrent.CompletionException;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.entity.Player;
 | 
			
		||||
import org.bukkit.event.EventHandler;
 | 
			
		||||
import org.bukkit.event.Listener;
 | 
			
		||||
import org.bukkit.event.player.PlayerJoinEvent;
 | 
			
		||||
import org.bukkit.plugin.Plugin;
 | 
			
		||||
import org.bukkit.scheduler.BukkitRunnable;
 | 
			
		||||
 | 
			
		||||
import eu.m724.jarupdater.Updater;
 | 
			
		||||
import eu.m724.jarupdater.object.Version;
 | 
			
		||||
import eu.m724.realweather.DebugLogger;
 | 
			
		||||
import eu.m724.realweather.GlobalConstants;
 | 
			
		||||
 | 
			
		||||
public class UpdateNotifier extends BukkitRunnable implements Listener { // TODO move this to jarupdater
 | 
			
		||||
	private Updater updater;
 | 
			
		||||
	private Consumer<Version> updateConsumer;
 | 
			
		||||
	
 | 
			
		||||
	private Plugin plugin = GlobalConstants.getPlugin();
 | 
			
		||||
	private Version latestVersion;
 | 
			
		||||
	
 | 
			
		||||
	public UpdateNotifier(Updater updater, Consumer<Version> updateConsumer) {
 | 
			
		||||
		this.updater = updater;
 | 
			
		||||
		this.updateConsumer = updateConsumer;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public void register() {
 | 
			
		||||
		this.runTaskTimerAsynchronously(plugin, 0, 432000); // 6h
 | 
			
		||||
		plugin.getServer().getPluginManager().registerEvents(this, plugin);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
		
 | 
			
		||||
		try {
 | 
			
		||||
			latestVersion = updater.getLatestVersion().join();
 | 
			
		||||
		} catch (CompletionException e) {
 | 
			
		||||
			Throwable ex = e.getCause();
 | 
			
		||||
			
 | 
			
		||||
			if (ex instanceof IOException) 
 | 
			
		||||
				DebugLogger.info("error trying to contact update server: %d", 0, ex.getMessage());
 | 
			
		||||
			else e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		if (latestVersion == null) return;
 | 
			
		||||
		
 | 
			
		||||
		for (Player player : plugin.getServer().getOnlinePlayers()) {
 | 
			
		||||
			if (player.hasPermission("realweather.update.notify")) {
 | 
			
		||||
				player.sendMessage("RealWeather is outdated. /rwadmin update");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		updateConsumer.accept(latestVersion);
 | 
			
		||||
		
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	@EventHandler
 | 
			
		||||
	public void onPlayerJoin(PlayerJoinEvent e) {
 | 
			
		||||
		Player player = e.getPlayer();
 | 
			
		||||
		if (latestVersion != null && player.hasPermission("realweather.update.notify")) {
 | 
			
		||||
			player.sendMessage("RealWeather is outdated. /rwadmin update");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,198 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.StandardCopyOption;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.concurrent.CompletionException;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.entity.Player;
 | 
			
		||||
import org.bukkit.event.EventHandler;
 | 
			
		||||
import org.bukkit.event.Listener;
 | 
			
		||||
import org.bukkit.event.player.PlayerJoinEvent;
 | 
			
		||||
import org.bukkit.plugin.Plugin;
 | 
			
		||||
import org.bukkit.scheduler.BukkitRunnable;
 | 
			
		||||
import eu.m724.realweather.DebugLogger;
 | 
			
		||||
import eu.m724.realweather.GlobalConstants;
 | 
			
		||||
import eu.m724.realweather.exception.UserError;
 | 
			
		||||
import eu.m724.realweather.updater.metadata.MetadataRetriever;
 | 
			
		||||
import eu.m724.realweather.updater.metadata.MetadataServerException;
 | 
			
		||||
import eu.m724.realweather.updater.metadata.VersionMetadata;
 | 
			
		||||
import net.md_5.bungee.api.ChatColor;
 | 
			
		||||
import net.md_5.bungee.api.chat.BaseComponent;
 | 
			
		||||
import net.md_5.bungee.api.chat.ComponentBuilder;
 | 
			
		||||
 | 
			
		||||
public class Updater extends BukkitRunnable implements Listener {
 | 
			
		||||
	private UpdaterConfig updaterConfig;
 | 
			
		||||
	private MetadataRetriever metadataRetriever;
 | 
			
		||||
	private UpdateDownloader updateDownloader;
 | 
			
		||||
	
 | 
			
		||||
	private Plugin plugin = GlobalConstants.getPlugin();
 | 
			
		||||
	
 | 
			
		||||
	private File jarFile;
 | 
			
		||||
	private CompletableFuture<File> downloadFuture;
 | 
			
		||||
	
 | 
			
		||||
	private VersionMetadata currentMetadata;
 | 
			
		||||
	private VersionMetadata latestMetadata;
 | 
			
		||||
	private long checkCacheExpires;
 | 
			
		||||
	
 | 
			
		||||
	private BaseComponent updateMessage = null;
 | 
			
		||||
	private BaseComponent updateActionMessage =
 | 
			
		||||
			new ComponentBuilder("To update: ").color(ChatColor.YELLOW)
 | 
			
		||||
			.append("/rwadmin update").color(ChatColor.AQUA)
 | 
			
		||||
			.build();
 | 
			
		||||
	
 | 
			
		||||
	public Updater(UpdaterConfig updaterConfig) {
 | 
			
		||||
		this.updaterConfig = updaterConfig;
 | 
			
		||||
		this.metadataRetriever = new MetadataRetriever(plugin, updaterConfig.channel);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public void init() {
 | 
			
		||||
		try {
 | 
			
		||||
			DebugLogger.info("probing for channels...", 1);
 | 
			
		||||
			
 | 
			
		||||
			List<String> channels = metadataRetriever.getChannels().join();
 | 
			
		||||
			if (!channels.contains(updaterConfig.channel)) {
 | 
			
		||||
				throw new UserError( // TODO replace this with its own error / exception?
 | 
			
		||||
						"Invalid channel: %s. Valid ones are: %s"
 | 
			
		||||
						.formatted(updaterConfig.channel, String.join(", ", channels))); // WHY DID NOBODY TELL ME ABOUT .formatted
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			currentMetadata = metadataRetriever.getCurrentVersionMetadata().join();
 | 
			
		||||
			DebugLogger.info("current: %d", 1, currentMetadata.id);
 | 
			
		||||
		} catch (CompletionException e) {
 | 
			
		||||
			int statusCode = ((MetadataServerException) e.getCause()).getStatusCode();
 | 
			
		||||
			if (statusCode >= 500) {
 | 
			
		||||
				DebugLogger.info("Unable to contact the update server! %d", 0, statusCode);
 | 
			
		||||
				DebugLogger.info("As it's a server error, it's probably temporary and not a configuration issue, so proceeding.", 0);
 | 
			
		||||
			} else {
 | 
			
		||||
				DebugLogger.info("Update server returned unexpected status code: %d", 0, statusCode);
 | 
			
		||||
				if (plugin.getDescription().getVersion().endsWith("SNAPSHOT")) {
 | 
			
		||||
					DebugLogger.info("Ignore as you're running a development snapshot.", 0);
 | 
			
		||||
				} else {
 | 
			
		||||
					DebugLogger.info("This is probably critical. RealWeather will continue without updater.", 0);
 | 
			
		||||
					DebugLogger.info("Try restarting the server. If that doesn't help, contact support.", 0);
 | 
			
		||||
				}
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		try {
 | 
			
		||||
			jarFile = new File(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
 | 
			
		||||
		} catch (URISyntaxException e) { }
 | 
			
		||||
		updateDownloader = new UpdateDownloader();
 | 
			
		||||
		
 | 
			
		||||
		if (updaterConfig.notify) {
 | 
			
		||||
			this.runTaskTimerAsynchronously(plugin, 0, 216000); // 3h
 | 
			
		||||
			plugin.getServer().getPluginManager().registerEvents(this, plugin);
 | 
			
		||||
			DebugLogger.info("updater will notify", 1);
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		DebugLogger.info("updater loaded", 1);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * download latest update
 | 
			
		||||
	 * @return null if no update else a future with the file (it can error with ioexcepton or signatureexception if signanture invalid)
 | 
			
		||||
	 */
 | 
			
		||||
	public CompletableFuture<File> downloadUpdate() {
 | 
			
		||||
		if (latestMetadata == null)
 | 
			
		||||
			return null;
 | 
			
		||||
		
 | 
			
		||||
		String url = metadataRetriever.getFileUrl(latestMetadata.label, latestMetadata.fileUrl);
 | 
			
		||||
		downloadFuture = updateDownloader.downloadAndVerify(url, latestMetadata.sha256);
 | 
			
		||||
		
 | 
			
		||||
		return downloadFuture;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public boolean installUpdate() {
 | 
			
		||||
		File downloadedFile = null;
 | 
			
		||||
		try {
 | 
			
		||||
			downloadedFile = downloadFuture.join();
 | 
			
		||||
			// TODO what if we changed File to Path and every ref in this file
 | 
			
		||||
			Files.move(downloadedFile.toPath(), jarFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
 | 
			
		||||
		} catch (CompletionException | IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
			return false;
 | 
			
		||||
		} finally {
 | 
			
		||||
			downloadFuture = null;
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public CompletableFuture<VersionMetadata> getLatestVersion() {
 | 
			
		||||
		if (System.currentTimeMillis() < checkCacheExpires)
 | 
			
		||||
			return CompletableFuture.completedFuture(latestMetadata);
 | 
			
		||||
		else
 | 
			
		||||
			return checkForNewVersion().handle((result, ex) -> null);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * this can throw completionexception
 | 
			
		||||
	 * @return
 | 
			
		||||
	 */
 | 
			
		||||
	private CompletableFuture<VersionMetadata> checkForNewVersion() {
 | 
			
		||||
		CompletableFuture<VersionMetadata> latestMetadataFuture =
 | 
			
		||||
				metadataRetriever.getLatestVersionMetadata();
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<VersionMetadata> newMetadataFuture =
 | 
			
		||||
				latestMetadataFuture.thenApply(latestMetadata -> {
 | 
			
		||||
					DebugLogger.info("Current version: %s (%d)", 2, currentMetadata.label, currentMetadata.id);
 | 
			
		||||
					DebugLogger.info("Latest version: %s (%d)", 2, latestMetadata.label, latestMetadata.id);
 | 
			
		||||
					
 | 
			
		||||
					if (currentMetadata.id < latestMetadata.id) {
 | 
			
		||||
						this.latestMetadata = latestMetadata;
 | 
			
		||||
						this.checkCacheExpires = System.currentTimeMillis() + 10800000;
 | 
			
		||||
						return latestMetadata;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					this.checkCacheExpires = System.currentTimeMillis() + 3600000;
 | 
			
		||||
					return null;
 | 
			
		||||
				});
 | 
			
		||||
		
 | 
			
		||||
		return newMetadataFuture;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	@EventHandler
 | 
			
		||||
	public void onPlayerJoin(PlayerJoinEvent event) {
 | 
			
		||||
		Player player = event.getPlayer();
 | 
			
		||||
		if (!player.hasPermission("realweather.update.notify")) return;
 | 
			
		||||
		
 | 
			
		||||
		if (updateMessage != null) { // TODO make changelog somewhere
 | 
			
		||||
			player.spigot().sendMessage(updateMessage);
 | 
			
		||||
			if (player.hasPermission("realweather.admin"))
 | 
			
		||||
				player.spigot().sendMessage(updateActionMessage);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
		DebugLogger.info("Checking for update...", 2);
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<VersionMetadata> newVersionFuture = checkForNewVersion();
 | 
			
		||||
		
 | 
			
		||||
		try {
 | 
			
		||||
			newVersionFuture.join();
 | 
			
		||||
		} catch (CompletionException e) {
 | 
			
		||||
			int statusCode = ((MetadataServerException) e.getCause()).getStatusCode();
 | 
			
		||||
			DebugLogger.info("Couldn't check for updates: %d", 0, statusCode);
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		updateMessage = new ComponentBuilder("An update is available!\n")
 | 
			
		||||
				.color(ChatColor.YELLOW)
 | 
			
		||||
				.append("RealWeather ").color(ChatColor.GOLD)
 | 
			
		||||
				.append(latestMetadata.label).color(ChatColor.AQUA).bold(true)
 | 
			
		||||
				.append(" released ").color(ChatColor.GOLD)
 | 
			
		||||
				.append(latestMetadata.getFormattedDate()).color(ChatColor.AQUA)
 | 
			
		||||
				.append("\nCurrent: ").color(ChatColor.GRAY)
 | 
			
		||||
				.append(currentMetadata.label).color(ChatColor.DARK_AQUA)
 | 
			
		||||
				.build();
 | 
			
		||||
		
 | 
			
		||||
		DebugLogger.info(updateMessage.toPlainText(), 0);
 | 
			
		||||
		DebugLogger.info(updateActionMessage.toPlainText(), 0);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,110 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater.metadata;
 | 
			
		||||
 | 
			
		||||
import java.net.ProxySelector;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.http.HttpClient;
 | 
			
		||||
import java.net.http.HttpRequest;
 | 
			
		||||
import java.net.http.HttpResponse;
 | 
			
		||||
import java.net.http.HttpClient.Redirect;
 | 
			
		||||
import java.net.http.HttpResponse.BodyHandlers;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import java.util.concurrent.CompletionException;
 | 
			
		||||
 | 
			
		||||
import org.bukkit.plugin.Plugin;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.Gson;
 | 
			
		||||
 | 
			
		||||
public class MetadataRetriever {
 | 
			
		||||
	private String version;
 | 
			
		||||
	private String channel;
 | 
			
		||||
	
 | 
			
		||||
	private CompletableFuture<VersionMetadata> currentMetadataCached = null;
 | 
			
		||||
	
 | 
			
		||||
	public MetadataRetriever(Plugin plugin, String channel) {
 | 
			
		||||
		this.version = plugin.getDescription().getVersion();
 | 
			
		||||
		this.channel = channel;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public String getFileUrl(String version, String file) {
 | 
			
		||||
		return "https://git.724.rocks/Minecon724/realweather-metadata/raw/branch/master/data/%s/%s/%s"
 | 
			
		||||
				.formatted(channel, version, file);
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * the completablefuture can throw a completionexception with {@link eu.m724.realweather.updater.metadata.MetadataServerException}
 | 
			
		||||
	 */
 | 
			
		||||
	private CompletableFuture<VersionMetadata> getMetadataOf(String version) {
 | 
			
		||||
		String url = getFileUrl(version, "meta-v1.json");
 | 
			
		||||
		
 | 
			
		||||
		HttpRequest request = HttpRequest.newBuilder()
 | 
			
		||||
				.uri(URI.create(url))
 | 
			
		||||
				.header("User-Agent", "rwu/1") // real weather updater v1
 | 
			
		||||
				.build();
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<HttpResponse<String>> responseFuture =
 | 
			
		||||
				HttpClient.newBuilder()
 | 
			
		||||
				.followRedirects(Redirect.NORMAL)
 | 
			
		||||
				.proxy(ProxySelector.getDefault()).build().
 | 
			
		||||
				sendAsync(request, BodyHandlers.ofString());
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<VersionMetadata> metadataFuture =
 | 
			
		||||
				responseFuture.thenApply(response -> {
 | 
			
		||||
					if (response.statusCode() != 200)
 | 
			
		||||
						throw new CompletionException(new MetadataServerException(response.statusCode()));
 | 
			
		||||
					
 | 
			
		||||
					VersionMetadata versionMetadata = new Gson().fromJson(response.body(), VersionMetadata.class);
 | 
			
		||||
					return versionMetadata;
 | 
			
		||||
				});
 | 
			
		||||
		
 | 
			
		||||
		return metadataFuture;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public CompletableFuture<List<String>> getChannels() {
 | 
			
		||||
		String url = "https://git.724.rocks/Minecon724/realweather-metadata/raw/branch/master/data/channels.txt";
 | 
			
		||||
		
 | 
			
		||||
		HttpRequest request = HttpRequest.newBuilder()
 | 
			
		||||
				.uri(URI.create(url))
 | 
			
		||||
				.header("User-Agent", "rwu/1") // real weather updater v1
 | 
			
		||||
				.build();
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<HttpResponse<String>> responseFuture =
 | 
			
		||||
				HttpClient.newBuilder()
 | 
			
		||||
				.followRedirects(Redirect.NORMAL)
 | 
			
		||||
				.proxy(ProxySelector.getDefault()).build().
 | 
			
		||||
				sendAsync(request, BodyHandlers.ofString());
 | 
			
		||||
		
 | 
			
		||||
		CompletableFuture<List<String>> channelsFuture =
 | 
			
		||||
				responseFuture.thenApply(response -> {
 | 
			
		||||
					if (response.statusCode() != 200)
 | 
			
		||||
						throw new CompletionException(new MetadataServerException(response.statusCode()));
 | 
			
		||||
					
 | 
			
		||||
					return response.body().lines().toList();
 | 
			
		||||
				});
 | 
			
		||||
		
 | 
			
		||||
		return channelsFuture; // TODO remove repeated code?
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * the {@link CompletableFuture} can throw a {@link CompletionException} with {@link MetadataServerException}
 | 
			
		||||
	 */
 | 
			
		||||
	public CompletableFuture<VersionMetadata> getLatestVersionMetadata() {
 | 
			
		||||
		return getVersionMetadata("latest");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * the completablefuture can throw a completionexception with {@link MetadataServerException}
 | 
			
		||||
	 */
 | 
			
		||||
	public CompletableFuture<VersionMetadata> getCurrentVersionMetadata() {
 | 
			
		||||
		if (currentMetadataCached == null)
 | 
			
		||||
			currentMetadataCached = getVersionMetadata(version);
 | 
			
		||||
		return currentMetadataCached; // TODO reconsider this
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * the completablefuture can throw a completionexception with {@link MetadataServerException}
 | 
			
		||||
	 */
 | 
			
		||||
	public CompletableFuture<VersionMetadata> getVersionMetadata(String version) {
 | 
			
		||||
		return getMetadataOf(version); // TODO remove this and rename that function?
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater.metadata;
 | 
			
		||||
 | 
			
		||||
public class MetadataServerException extends Exception {
 | 
			
		||||
	private int statusCode;
 | 
			
		||||
 | 
			
		||||
	private static final long serialVersionUID = -782470406494196579L;
 | 
			
		||||
	
 | 
			
		||||
	public MetadataServerException(int statusCode) {
 | 
			
		||||
		this.statusCode = statusCode;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	public int getStatusCode() {
 | 
			
		||||
		return statusCode;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
package eu.m724.realweather.updater.metadata;
 | 
			
		||||
 | 
			
		||||
import java.text.SimpleDateFormat;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
 | 
			
		||||
public class VersionMetadata {
 | 
			
		||||
	/**
 | 
			
		||||
	 * metadata file version
 | 
			
		||||
	 */
 | 
			
		||||
	public final int spec = 1;
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * version id. increments with each version
 | 
			
		||||
	 */
 | 
			
		||||
	public int id;
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * release time of a version
 | 
			
		||||
	 */
 | 
			
		||||
	public long timestamp;
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * label aka version string
 | 
			
		||||
	 * example: 1.0.0
 | 
			
		||||
	 */
 | 
			
		||||
	public String label;
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * url of the downloadable jar file
 | 
			
		||||
	 */
 | 
			
		||||
	public String fileUrl;
 | 
			
		||||
	
 | 
			
		||||
	public String sha256;
 | 
			
		||||
	
 | 
			
		||||
	public String getFormattedDate() {
 | 
			
		||||
		return new SimpleDateFormat("dd.MM").format(new Date(timestamp));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue