This commit is contained in:
Braydon 2024-04-27 00:56:22 -04:00
parent 27696b41a2
commit caeea1620e
7 changed files with 284 additions and 3 deletions

View File

@ -1,2 +0,0 @@
# PIA-ServerList
A list of IPs for PIA servers.

28
pom.xml
View File

@ -55,13 +55,41 @@
</plugins>
</build>
<!-- Repos -->
<repositories>
<!-- Jitpack - Used for dnsjava -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<!-- Dependencies -->
<dependencies>
<!-- Libraries -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>v3.5.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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;
}

View File

@ -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<PIAServer> 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<PIAServer> getNewServers() {
Set<PIAServer> 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<PIAServer> 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<List<PIAServer>>() {}.getType());
}
}
}

View File

@ -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();
}
}

View File

@ -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<PIAServer> 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("<total-servers>", String.valueOf(servers.size())); // Total servers variable
// Write the total servers per-region table
Map<String, Integer> regionCounts = new HashMap<>();
for (PIAServer server : servers) {
regionCounts.put(server.getRegion(), regionCounts.getOrDefault(server.getRegion(), 0) + 1);
}
contents = contents.replace("<region-table-entry>", 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();
}
}
}

View File

@ -0,0 +1,9 @@
![Servers](https://img.shields.io/badge/servers-<total-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 |
|----------------------|---------|
<region-table-entry>