From caeea1620e5ecd60eadb7e347f59637861184334 Mon Sep 17 00:00:00 2001 From: Rainnny7 Date: Sat, 27 Apr 2024 00:56:22 -0400 Subject: [PATCH] code! --- README.md | 2 - pom.xml | 28 ++++ src/main/java/me/braydon/pia/PIAServer.java | 26 ++++ .../java/me/braydon/pia/PIAServerList.java | 128 +++++++++++++++++- .../me/braydon/pia/common/StringUtils.java | 37 +++++ .../me/braydon/pia/readme/ReadMeManager.java | 57 ++++++++ src/main/resources/README_TEMPLATE.md | 9 ++ 7 files changed, 284 insertions(+), 3 deletions(-) delete mode 100644 README.md create mode 100644 src/main/java/me/braydon/pia/PIAServer.java create mode 100644 src/main/java/me/braydon/pia/common/StringUtils.java create mode 100644 src/main/java/me/braydon/pia/readme/ReadMeManager.java create mode 100644 src/main/resources/README_TEMPLATE.md diff --git a/README.md b/README.md deleted file mode 100644 index 484f95d7a..000000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# PIA-ServerList -A list of IPs for PIA servers. diff --git a/pom.xml b/pom.xml index 08204cd66..1c03164bc 100644 --- a/pom.xml +++ b/pom.xml @@ -55,13 +55,41 @@ + + + + + jitpack.io + https://jitpack.io + + + + org.projectlombok lombok 1.18.32 provided + + com.google.code.gson + gson + 2.10.1 + compile + + + net.lingala.zip4j + zip4j + 2.11.5 + compile + + + com.github.dnsjava + dnsjava + v3.5.2 + compile + \ No newline at end of file diff --git a/src/main/java/me/braydon/pia/PIAServer.java b/src/main/java/me/braydon/pia/PIAServer.java new file mode 100644 index 000000000..055ca42df --- /dev/null +++ b/src/main/java/me/braydon/pia/PIAServer.java @@ -0,0 +1,26 @@ +package me.braydon.pia; + +import lombok.*; + +/** + * A representation of a PIA server. + * + * @author Braydon + */ +@AllArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString +public final class PIAServer { + /** + * The IPv4 address of this server. + */ + @EqualsAndHashCode.Include @NonNull private final String ip; + + /** + * The region of this server. + */ + @NonNull private final String region; + + /** + * The unix time of when this server was last seen. + */ + private final long lastSeen; +} \ No newline at end of file diff --git a/src/main/java/me/braydon/pia/PIAServerList.java b/src/main/java/me/braydon/pia/PIAServerList.java index d8d3be352..dab31ea99 100644 --- a/src/main/java/me/braydon/pia/PIAServerList.java +++ b/src/main/java/me/braydon/pia/PIAServerList.java @@ -1,12 +1,138 @@ package me.braydon.pia; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import lombok.NonNull; +import lombok.SneakyThrows; +import me.braydon.pia.readme.ReadMeManager; +import net.lingala.zip4j.ZipFile; +import org.xbill.DNS.ARecord; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.Type; + +import java.io.*; +import java.net.URL; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.TimeUnit; /** * @author Braydon */ public final class PIAServerList { + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .create(); + private static final String OPENVPN_FILES_ENDPOINT = "https://www.privateinternetaccess.com/openvpn/openvpn.zip"; + private static final File SERVERS_CONTEXT_FILE = new File("context.json"); + + @SneakyThrows public static void main(@NonNull String[] args) { - System.out.println("Hello World!"); + Set servers = getNewServers(); // Get the new servers from PIA + int before = servers.size(); + servers.addAll(loadServersFromContext()); // Load servers from context + System.out.println("Loaded " + (servers.size() - before) + " server(s) from the context file"); + + // Delete servers that haven't been seen in more than a week + before = servers.size(); + servers.removeIf(server -> (System.currentTimeMillis() - server.getLastSeen()) >= TimeUnit.DAYS.toMillis(7L)); + System.out.println("Removed " + (before - servers.size()) + " server(s) that haven't been seen in more than a week"); + + // Write the servers to the context file + System.out.println("Writing context file..."); + try (FileWriter fileWriter = new FileWriter(SERVERS_CONTEXT_FILE)) { + GSON.toJson(servers, fileWriter); + } + System.out.println("Done, wrote " + servers.size() + " servers to the file"); + + // Update the README.md file + ReadMeManager.update(servers); + } + + /** + * Get the new servers from the + * OpenVPN files provided by PIA. + * + * @return the new servers + */ + @SneakyThrows + private static Set getNewServers() { + Set servers = new HashSet<>(); // The new servers to return + File serversZip = new File("servers.zip"); // The zip file containing the servers + + // Download the OpenVPN servers zip from PIA + long before = System.currentTimeMillis(); + System.out.println("Downloading OpenVPN files from PIA..."); + try (InputStream inputStream = new URL(OPENVPN_FILES_ENDPOINT).openStream(); + BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); + FileOutputStream fileOutputStream = new FileOutputStream(serversZip) + ) { + byte[] dataBuffer = new byte[1024]; + int bytesRead; + while ((bytesRead = bufferedInputStream.read(dataBuffer, 0, 1024)) != -1) { + fileOutputStream.write(dataBuffer, 0, bytesRead); + } + } + assert serversZip.exists(); // Confirm the zip exists + System.out.println("Downloaded in " + (System.currentTimeMillis() - before) + "ms, extracting..."); + + // Extract the servers zip files + before = System.currentTimeMillis(); + try (ZipFile zip = new ZipFile(serversZip)) { + zip.extractAll("servers"); + } + serversZip.delete(); // Delete the zip file after extraction + System.out.println("Extracted in " + (System.currentTimeMillis() - before) + "ms"); + + // Iterate over the OpenVPN files downloaded from PIA + File serversDir = new File("servers"); // The dir where the downloaded OpenVPN files are stored + File[] openVpnFiles = serversDir.listFiles(); + assert openVpnFiles != null; + System.out.println("Found " + openVpnFiles.length + " OpenVPN files, reading them..."); + + for (File file : openVpnFiles) { + String region = file.getName().split("\\.")[0]; // The server region + try { + for (String line : Files.readAllLines(file.toPath())) { + // Line doesn't contain the remote server, ignore it + if (!line.startsWith("remote ")) { + continue; + } + Record[] records = new Lookup(line.split(" ")[1], Type.A).run(); // Resolve A records + if (records == null) { // No A records resolved + continue; + } + System.out.println("Resolved " + records.length + " A Records for region " + region); + for (Record record : records) { + servers.add(new PIAServer(((ARecord) record).getAddress().getHostAddress(), region, System.currentTimeMillis())); + } + } + } finally { + file.delete(); // Delete the OpenVPN file after reading it + } + } + serversDir.delete(); // Delete the servers dir after reading the OpenVPN files + + return servers; + } + + /** + * Load the servers from the context file. + * + * @return the loaded servers + */ + @SneakyThrows + private static List loadServersFromContext() { + if (!SERVERS_CONTEXT_FILE.exists()) { // No context file to load + return new ArrayList<>(); + } + try (FileReader fileReader = new FileReader(SERVERS_CONTEXT_FILE); + JsonReader jsonReader = new JsonReader(fileReader) + ) { + return GSON.fromJson(jsonReader, new TypeToken>() {}.getType()); + } } } \ No newline at end of file diff --git a/src/main/java/me/braydon/pia/common/StringUtils.java b/src/main/java/me/braydon/pia/common/StringUtils.java new file mode 100644 index 000000000..ef54132fb --- /dev/null +++ b/src/main/java/me/braydon/pia/common/StringUtils.java @@ -0,0 +1,37 @@ +package me.braydon.pia.common; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +/** + * @author Braydon + */ +@UtilityClass +public final class StringUtils { + /** + * Capitalize the first character + * in each word in the given string. + * + * @param input the input to capitalize + * @return the capitalized string + */ + @NonNull + public static String capitalizeFully(@NonNull String input, char delimiter) { + StringBuilder builder = new StringBuilder(); + for (String part : input.split(String.valueOf(delimiter))) { + builder.append(part.length() <= 3 ? part.toUpperCase() : capitalize(part)).append(delimiter); + } + return builder.substring(0, builder.length() - 1).replace(delimiter, ' '); + } + + /** + * Capitalize the first character in the given string. + * + * @param input the input to capitalize + * @return the capitalized string + */ + @NonNull + public static String capitalize(@NonNull String input) { + return Character.toUpperCase(input.charAt(0)) + input.substring(1).toLowerCase(); + } +} \ No newline at end of file diff --git a/src/main/java/me/braydon/pia/readme/ReadMeManager.java b/src/main/java/me/braydon/pia/readme/ReadMeManager.java new file mode 100644 index 000000000..34d631dbd --- /dev/null +++ b/src/main/java/me/braydon/pia/readme/ReadMeManager.java @@ -0,0 +1,57 @@ +package me.braydon.pia.readme; + +import lombok.NonNull; +import me.braydon.pia.PIAServer; +import me.braydon.pia.common.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Braydon + */ +public final class ReadMeManager { + /** + * Copy the template README.md file from the jar + * and update it with data from the given servers. + * + * @param servers the server data + */ + public static void update(@NonNull Set servers) { + System.out.println("Updating README.md..."); + try (InputStream templateResource = ReadMeManager.class.getClassLoader().getResourceAsStream("README_TEMPLATE.md")) { + assert templateResource != null; // Ensure the template is present + + // Copy the template README.md to the root directory + Path localReadMe = new File("README.md").toPath(); // The local README.md file + Files.copy(templateResource, localReadMe, StandardCopyOption.REPLACE_EXISTING); + + // Replace variables in the README.md file + String contents = new String(Files.readAllBytes(localReadMe)); + contents = contents.replace("", String.valueOf(servers.size())); // Total servers variable + + // Write the total servers per-region table + Map regionCounts = new HashMap<>(); + for (PIAServer server : servers) { + regionCounts.put(server.getRegion(), regionCounts.getOrDefault(server.getRegion(), 0) + 1); + } + contents = contents.replace("", regionCounts.entrySet().stream() + .map(entry -> "| " + StringUtils.capitalizeFully(entry.getKey(), '_') + " | " + entry.getValue() + " |") // Map the region to the count + .reduce((a, b) -> a + "\n" + b).orElse("")); // Reduce the entries to a single string + + // Write the contents to the file + Files.write(localReadMe, contents.getBytes()); + System.out.println("Done!"); + } catch (IOException ex) { + System.err.println("Failed to update the README.md file"); + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/README_TEMPLATE.md b/src/main/resources/README_TEMPLATE.md new file mode 100644 index 000000000..7d6f9624e --- /dev/null +++ b/src/main/resources/README_TEMPLATE.md @@ -0,0 +1,9 @@ +![Servers](https://img.shields.io/badge/servers--darkgreen) + +# PIA-ServerList +An automatically updated list of IPs for PIA servers, this list is updated every hour, and servers in this list will be removed in they have not been seen in the last week. + +## Servers +| Region | Servers | +|----------------------|---------| + \ No newline at end of file