first commit
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>me.proxylink</groupId>
|
||||
<artifactId>dirtsimplep2p</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>proxylink-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
</project>
|
||||
@@ -0,0 +1,206 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public record AgentConfig(
|
||||
Role role,
|
||||
String nodeName,
|
||||
TunnelConfig tunnel,
|
||||
TlsConfig tls,
|
||||
List<RouteConfig> routes
|
||||
) {
|
||||
private static final Pattern ROUTE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
|
||||
|
||||
public AgentConfig {
|
||||
Objects.requireNonNull(role, "role");
|
||||
nodeName = requireNonBlank(nodeName, "nodeName");
|
||||
Objects.requireNonNull(tunnel, "tunnel");
|
||||
Objects.requireNonNull(tls, "tls");
|
||||
routes = List.copyOf(Objects.requireNonNull(routes, "routes"));
|
||||
}
|
||||
|
||||
public void validateOrThrow() {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (routes.isEmpty()) {
|
||||
errors.add("At least one route is required");
|
||||
}
|
||||
|
||||
if (tunnel.authToken().length() < 32) {
|
||||
errors.add("tunnel.authToken must be at least 32 characters");
|
||||
}
|
||||
if (isPlaceholderSecret(tunnel.authToken())) {
|
||||
errors.add("tunnel.authToken is still the default placeholder and must be changed");
|
||||
}
|
||||
|
||||
if (!tls.enabled() && !tunnel.allowInsecure()) {
|
||||
errors.add("TLS is disabled, so tunnel.tls.allowInsecure must be true for explicit local testing");
|
||||
}
|
||||
|
||||
if (role == Role.FRONTEND) {
|
||||
validateHostPort(errors, "tunnel.listen", tunnel.listenHost(), tunnel.listenPort());
|
||||
if (tls.enabled() && !tls.autoGenerate()) {
|
||||
requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
|
||||
requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
|
||||
}
|
||||
}
|
||||
|
||||
if (role == Role.BACKEND) {
|
||||
validateHostPort(errors, "tunnel.connect", tunnel.connectHost(), tunnel.connectPort());
|
||||
if (tls.enabled()) {
|
||||
if (tls.trustStorePath() == null && tls.pinnedCertificateSha256().isBlank() && !tls.trustOnFirstUse()) {
|
||||
errors.add("TLS is enabled on backend, so set tunnel.tls.pinnedCertificateSha256, set tunnel.tls.trustOnFirstUse=true, or configure a truststore");
|
||||
}
|
||||
if (tls.trustStorePath() != null) {
|
||||
requireSecret(errors, "tunnel.tls.trustStorePassword", tls.trustStorePassword());
|
||||
}
|
||||
if (tls.requireClientAuth()) {
|
||||
requirePath(errors, "tunnel.tls.keyStore", tls.keyStorePath());
|
||||
requireSecret(errors, "tunnel.tls.keyStorePassword", tls.keyStorePassword());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tunnel.connectTimeoutMillis() < 1000) {
|
||||
errors.add("tunnel.connectTimeoutMillis must be at least 1000");
|
||||
}
|
||||
if (tunnel.heartbeatIntervalMillis() < 1000) {
|
||||
errors.add("tunnel.heartbeatIntervalMillis must be at least 1000");
|
||||
}
|
||||
if (tunnel.heartbeatTimeoutMillis() <= tunnel.heartbeatIntervalMillis()) {
|
||||
errors.add("tunnel.heartbeatTimeoutMillis must be greater than tunnel.heartbeatIntervalMillis");
|
||||
}
|
||||
if (tunnel.heartbeatMissesBeforeDisconnect() < 2) {
|
||||
errors.add("tunnel.heartbeatMissesBeforeDisconnect must be at least 2");
|
||||
}
|
||||
if (tunnel.reconnectInitialMillis() < 250) {
|
||||
errors.add("tunnel.reconnectInitialMillis must be at least 250");
|
||||
}
|
||||
if (tunnel.reconnectMaxMillis() < tunnel.reconnectInitialMillis()) {
|
||||
errors.add("tunnel.reconnectMaxMillis must be greater than or equal to tunnel.reconnectInitialMillis");
|
||||
}
|
||||
|
||||
for (RouteConfig route : routes) {
|
||||
if (!ROUTE_NAME.matcher(route.name()).matches()) {
|
||||
errors.add("Route name contains invalid characters: " + route.name());
|
||||
}
|
||||
if (role == Role.FRONTEND) {
|
||||
validateHostPort(errors, "route." + route.name() + ".frontendBind", route.frontendBindHost(), route.frontendBindPort());
|
||||
}
|
||||
if (role == Role.BACKEND) {
|
||||
validateHostPort(errors, "route." + route.name() + ".backendTarget", route.backendTargetHost(), route.backendTargetPort());
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid configuration:\n - " + String.join("\n - ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
public RouteConfig requireRoute(String routeName) throws ProtocolException {
|
||||
for (RouteConfig route : routes) {
|
||||
if (route.name().equals(routeName)) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
throw new ProtocolException("Unknown route: " + routeName);
|
||||
}
|
||||
|
||||
private static void validateHostPort(List<String> errors, String keyPrefix, String host, int port) {
|
||||
if (host == null || host.isBlank()) {
|
||||
errors.add(keyPrefix + "Host must not be blank");
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
errors.add(keyPrefix + "Port must be between 1 and 65535");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePath(List<String> errors, String key, Path path) {
|
||||
if (path == null) {
|
||||
errors.add(key + " is required when TLS is enabled");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireSecret(List<String> errors, String key, String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
errors.add(key + " is required when TLS is enabled");
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireNonBlank(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " must not be blank");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean isPlaceholderSecret(String value) {
|
||||
String normalized = value.toLowerCase(Locale.ROOT);
|
||||
return normalized.contains("change_me")
|
||||
|| normalized.contains("replace-with")
|
||||
|| normalized.contains("placeholder");
|
||||
}
|
||||
|
||||
public enum Role {
|
||||
FRONTEND,
|
||||
BACKEND;
|
||||
|
||||
public static Role parse(String value) {
|
||||
return Role.valueOf(value.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
public record TunnelConfig(
|
||||
String listenHost,
|
||||
int listenPort,
|
||||
String connectHost,
|
||||
int connectPort,
|
||||
String authToken,
|
||||
boolean allowInsecure,
|
||||
int connectTimeoutMillis,
|
||||
long heartbeatIntervalMillis,
|
||||
long heartbeatTimeoutMillis,
|
||||
int heartbeatMissesBeforeDisconnect,
|
||||
long reconnectInitialMillis,
|
||||
long reconnectMaxMillis
|
||||
) {
|
||||
public TunnelConfig {
|
||||
authToken = requireNonBlank(authToken, "authToken");
|
||||
}
|
||||
}
|
||||
|
||||
public record TlsConfig(
|
||||
boolean enabled,
|
||||
Path keyStorePath,
|
||||
String keyStorePassword,
|
||||
Path trustStorePath,
|
||||
String trustStorePassword,
|
||||
boolean requireClientAuth,
|
||||
boolean autoGenerate,
|
||||
boolean trustOnFirstUse,
|
||||
String pinnedCertificateSha256
|
||||
) {
|
||||
public TlsConfig {
|
||||
keyStorePassword = keyStorePassword == null ? "" : keyStorePassword;
|
||||
trustStorePassword = trustStorePassword == null ? "" : trustStorePassword;
|
||||
pinnedCertificateSha256 = pinnedCertificateSha256 == null ? "" : pinnedCertificateSha256.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public record RouteConfig(
|
||||
String name,
|
||||
String frontendBindHost,
|
||||
int frontendBindPort,
|
||||
String backendTargetHost,
|
||||
int backendTargetPort
|
||||
) {
|
||||
public RouteConfig {
|
||||
name = requireNonBlank(name, "name");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public final class AgentConfigIO {
|
||||
private AgentConfigIO() {
|
||||
}
|
||||
|
||||
public static AgentConfig load(Path path) throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream input = Files.newInputStream(path)) {
|
||||
properties.load(input);
|
||||
}
|
||||
Path baseDir = path.toAbsolutePath().getParent();
|
||||
return fromProperties(properties, baseDir);
|
||||
}
|
||||
|
||||
public static AgentConfig fromProperties(Properties properties) {
|
||||
return fromProperties(properties, null);
|
||||
}
|
||||
|
||||
public static AgentConfig fromProperties(Properties properties, Path baseDir) {
|
||||
AgentConfig.Role role = AgentConfig.Role.parse(required(properties, "role"));
|
||||
String nodeName = value(properties, "node.name", role.name().toLowerCase());
|
||||
|
||||
AgentConfig.TunnelConfig tunnel = new AgentConfig.TunnelConfig(
|
||||
value(properties, "tunnel.listenHost", "0.0.0.0"),
|
||||
intValue(properties, "tunnel.listenPort", -1),
|
||||
value(properties, "tunnel.connectHost", ""),
|
||||
intValue(properties, "tunnel.connectPort", -1),
|
||||
required(properties, "tunnel.authToken"),
|
||||
boolValue(properties, "tunnel.tls.allowInsecure", false),
|
||||
intValue(properties, "tunnel.connectTimeoutMillis", 5000),
|
||||
longValue(properties, "tunnel.heartbeatIntervalMillis", 2000L),
|
||||
longValue(properties, "tunnel.heartbeatTimeoutMillis", 8000L),
|
||||
intValue(properties, "tunnel.heartbeatMissesBeforeDisconnect", 4),
|
||||
longValue(properties, "tunnel.reconnectInitialMillis", 250L),
|
||||
longValue(properties, "tunnel.reconnectMaxMillis", 10000L)
|
||||
);
|
||||
|
||||
AgentConfig.TlsConfig tls = new AgentConfig.TlsConfig(
|
||||
boolValue(properties, "tunnel.tls.enabled", true),
|
||||
pathValue(properties, "tunnel.tls.keyStore", baseDir),
|
||||
value(properties, "tunnel.tls.keyStorePassword", ""),
|
||||
pathValue(properties, "tunnel.tls.trustStore", baseDir),
|
||||
value(properties, "tunnel.tls.trustStorePassword", ""),
|
||||
boolValue(properties, "tunnel.tls.requireClientAuth", false),
|
||||
boolValue(properties, "tunnel.tls.autoGenerate", true),
|
||||
boolValue(properties, "tunnel.tls.trustOnFirstUse", true),
|
||||
value(properties, "tunnel.tls.pinnedCertificateSha256", "")
|
||||
);
|
||||
|
||||
return new AgentConfig(role, nodeName, tunnel, tls, routes(properties));
|
||||
}
|
||||
|
||||
private static List<AgentConfig.RouteConfig> routes(Properties properties) {
|
||||
String routesValue = required(properties, "routes");
|
||||
List<AgentConfig.RouteConfig> routes = new ArrayList<>();
|
||||
for (String rawName : routesValue.split(",")) {
|
||||
String name = rawName.trim();
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String prefix = "route." + name + ".";
|
||||
routes.add(new AgentConfig.RouteConfig(
|
||||
name,
|
||||
value(properties, prefix + "frontendBindHost", "127.0.0.1"),
|
||||
intValue(properties, prefix + "frontendBindPort", -1),
|
||||
value(properties, prefix + "backendTargetHost", "127.0.0.1"),
|
||||
intValue(properties, prefix + "backendTargetPort", -1)
|
||||
));
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing required config key: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String value(Properties properties, String key, String defaultValue) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static int intValue(Properties properties, String key, int defaultValue) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Config key must be an integer: " + key, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static long longValue(Properties properties, String key, long defaultValue) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Config key must be a long integer: " + key, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean boolValue(Properties properties, String key, boolean defaultValue) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return Boolean.parseBoolean(value.trim());
|
||||
}
|
||||
|
||||
private static Path pathValue(Properties properties, String key, Path baseDir) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Path path = Path.of(value.trim());
|
||||
if (!path.isAbsolute() && baseDir != null) {
|
||||
return baseDir.resolve(path).normalize();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class AuthPayloads {
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
private AuthPayloads() {
|
||||
}
|
||||
|
||||
public record Hello(int protocolVersion, String role, String nodeName, byte[] clientNonce) {
|
||||
public Hello {
|
||||
Objects.requireNonNull(role, "role");
|
||||
Objects.requireNonNull(nodeName, "nodeName");
|
||||
clientNonce = copyNonce(clientNonce, "clientNonce");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] clientNonce() {
|
||||
return Arrays.copyOf(clientNonce, clientNonce.length);
|
||||
}
|
||||
}
|
||||
|
||||
public record Challenge(byte[] serverNonce) {
|
||||
public Challenge {
|
||||
serverNonce = copyNonce(serverNonce, "serverNonce");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serverNonce() {
|
||||
return Arrays.copyOf(serverNonce, serverNonce.length);
|
||||
}
|
||||
}
|
||||
|
||||
public record Response(byte[] hmac) {
|
||||
public Response {
|
||||
if (hmac == null || hmac.length != ProtocolConstants.AUTH_RESPONSE_BYTES) {
|
||||
throw new IllegalArgumentException("hmac must be " + ProtocolConstants.AUTH_RESPONSE_BYTES + " bytes");
|
||||
}
|
||||
hmac = Arrays.copyOf(hmac, hmac.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] hmac() {
|
||||
return Arrays.copyOf(hmac, hmac.length);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] randomNonce() {
|
||||
byte[] nonce = new byte[ProtocolConstants.AUTH_NONCE_BYTES];
|
||||
SECURE_RANDOM.nextBytes(nonce);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public static byte[] encodeHello(Hello hello) throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
DataOutputStream output = new DataOutputStream(bytes);
|
||||
output.writeInt(hello.protocolVersion());
|
||||
output.writeUTF(hello.role());
|
||||
output.writeUTF(hello.nodeName());
|
||||
output.write(hello.clientNonce());
|
||||
output.flush();
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
public static Hello decodeHello(byte[] payload) throws IOException {
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload));
|
||||
int protocolVersion = input.readInt();
|
||||
String role = input.readUTF();
|
||||
String nodeName = input.readUTF();
|
||||
byte[] clientNonce = input.readNBytes(ProtocolConstants.AUTH_NONCE_BYTES);
|
||||
if (clientNonce.length != ProtocolConstants.AUTH_NONCE_BYTES || input.available() != 0) {
|
||||
throw new ProtocolException("Invalid HELLO payload");
|
||||
}
|
||||
return new Hello(protocolVersion, role, nodeName, clientNonce);
|
||||
}
|
||||
|
||||
public static byte[] encodeChallenge(Challenge challenge) throws IOException {
|
||||
return challenge.serverNonce();
|
||||
}
|
||||
|
||||
public static Challenge decodeChallenge(byte[] payload) throws IOException {
|
||||
if (payload.length != ProtocolConstants.AUTH_NONCE_BYTES) {
|
||||
throw new ProtocolException("Invalid AUTH_CHALLENGE payload");
|
||||
}
|
||||
return new Challenge(payload);
|
||||
}
|
||||
|
||||
public static byte[] encodeResponse(Response response) {
|
||||
return response.hmac();
|
||||
}
|
||||
|
||||
public static Response decodeResponse(byte[] payload) throws IOException {
|
||||
if (payload.length != ProtocolConstants.AUTH_RESPONSE_BYTES) {
|
||||
throw new ProtocolException("Invalid AUTH_RESPONSE payload");
|
||||
}
|
||||
return new Response(payload);
|
||||
}
|
||||
|
||||
public static byte[] computeResponse(String authToken, Hello hello, Challenge challenge) throws ProtocolException {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(authToken.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
updateUtf8(mac, ProtocolConstants.AUTH_CONTEXT);
|
||||
updateUtf8(mac, hello.role());
|
||||
updateUtf8(mac, hello.nodeName());
|
||||
mac.update(hello.clientNonce());
|
||||
mac.update(challenge.serverNonce());
|
||||
return mac.doFinal();
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new ProtocolException("Unable to compute authentication response", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean constantTimeEquals(byte[] expected, byte[] actual) {
|
||||
return MessageDigest.isEqual(expected, actual);
|
||||
}
|
||||
|
||||
private static void updateUtf8(Mac mac, String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
mac.update((byte) ((bytes.length >>> 8) & 0xff));
|
||||
mac.update((byte) (bytes.length & 0xff));
|
||||
mac.update(bytes);
|
||||
}
|
||||
|
||||
private static byte[] copyNonce(byte[] nonce, String name) {
|
||||
if (nonce == null || nonce.length != ProtocolConstants.AUTH_NONCE_BYTES) {
|
||||
throw new IllegalArgumentException(name + " must be " + ProtocolConstants.AUTH_NONCE_BYTES + " bytes");
|
||||
}
|
||||
return Arrays.copyOf(nonce, nonce.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Frame {
|
||||
private static final byte[] EMPTY = new byte[0];
|
||||
|
||||
private final FrameType type;
|
||||
private final long streamId;
|
||||
private final byte[] payload;
|
||||
|
||||
public Frame(FrameType type, long streamId, byte[] payload) {
|
||||
this.type = Objects.requireNonNull(type, "type");
|
||||
this.streamId = streamId;
|
||||
this.payload = payload == null || payload.length == 0 ? EMPTY : Arrays.copyOf(payload, payload.length);
|
||||
}
|
||||
|
||||
public static Frame empty(FrameType type, long streamId) {
|
||||
return new Frame(type, streamId, EMPTY);
|
||||
}
|
||||
|
||||
public FrameType type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long streamId() {
|
||||
return streamId;
|
||||
}
|
||||
|
||||
public byte[] payload() {
|
||||
return payload.length == 0 ? EMPTY : Arrays.copyOf(payload, payload.length);
|
||||
}
|
||||
|
||||
public int payloadLength() {
|
||||
return payload.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public final class FrameCodec {
|
||||
private final DataInputStream input;
|
||||
private final DataOutputStream output;
|
||||
|
||||
public FrameCodec(InputStream input, OutputStream output) {
|
||||
this.input = new DataInputStream(input);
|
||||
this.output = new DataOutputStream(output);
|
||||
}
|
||||
|
||||
public Frame readFrame() throws IOException {
|
||||
int magic;
|
||||
try {
|
||||
magic = input.readInt();
|
||||
} catch (EOFException eof) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (magic != ProtocolConstants.MAGIC) {
|
||||
throw new ProtocolException("Invalid frame magic");
|
||||
}
|
||||
|
||||
int version = Short.toUnsignedInt(input.readShort());
|
||||
if (version != ProtocolConstants.PROTOCOL_VERSION) {
|
||||
throw new ProtocolException("Unsupported protocol version: " + version);
|
||||
}
|
||||
|
||||
int typeCode = Byte.toUnsignedInt(input.readByte());
|
||||
input.readByte();
|
||||
long streamId = input.readLong();
|
||||
int payloadLength = input.readInt();
|
||||
|
||||
if (payloadLength < 0 || payloadLength > ProtocolConstants.MAX_FRAME_PAYLOAD_BYTES) {
|
||||
throw new ProtocolException("Invalid frame payload length: " + payloadLength);
|
||||
}
|
||||
|
||||
byte[] payload = input.readNBytes(payloadLength);
|
||||
if (payload.length != payloadLength) {
|
||||
throw new EOFException("Unexpected end of stream while reading frame payload");
|
||||
}
|
||||
|
||||
return new Frame(FrameType.fromCode(typeCode), streamId, payload);
|
||||
}
|
||||
|
||||
public void writeFrame(Frame frame) throws IOException {
|
||||
byte[] payload = frame.payload();
|
||||
if (payload.length > ProtocolConstants.MAX_FRAME_PAYLOAD_BYTES) {
|
||||
throw new ProtocolException("Frame payload is too large: " + payload.length);
|
||||
}
|
||||
|
||||
synchronized (output) {
|
||||
output.writeInt(ProtocolConstants.MAGIC);
|
||||
output.writeShort(ProtocolConstants.PROTOCOL_VERSION);
|
||||
output.writeByte(frame.type().code());
|
||||
output.writeByte(0);
|
||||
output.writeLong(frame.streamId());
|
||||
output.writeInt(payload.length);
|
||||
output.write(payload);
|
||||
output.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum FrameType {
|
||||
HELLO(1),
|
||||
AUTH_CHALLENGE(2),
|
||||
AUTH_RESPONSE(3),
|
||||
AUTH_OK(4),
|
||||
AUTH_FAILED(5),
|
||||
PING(6),
|
||||
PONG(7),
|
||||
STREAM_OPEN(10),
|
||||
STREAM_DATA(11),
|
||||
STREAM_CLOSE(12),
|
||||
STREAM_RESET(13),
|
||||
ERROR(20),
|
||||
GOAWAY(21);
|
||||
|
||||
private static final Map<Integer, FrameType> BY_CODE = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (FrameType type : values()) {
|
||||
BY_CODE.put(type.code, type);
|
||||
}
|
||||
}
|
||||
|
||||
private final int code;
|
||||
|
||||
FrameType(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int code() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static FrameType fromCode(int code) throws ProtocolException {
|
||||
FrameType type = BY_CODE.get(code);
|
||||
if (type == null) {
|
||||
throw new ProtocolException("Unknown frame type: " + code);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
public final class ProtocolConstants {
|
||||
public static final int MAGIC = 0x44535032;
|
||||
public static final int PROTOCOL_VERSION = 1;
|
||||
public static final int MAX_FRAME_PAYLOAD_BYTES = 1024 * 1024;
|
||||
public static final int STREAM_DATA_CHUNK_BYTES = 16 * 1024;
|
||||
public static final int AUTH_NONCE_BYTES = 32;
|
||||
public static final int AUTH_RESPONSE_BYTES = 32;
|
||||
public static final String AUTH_CONTEXT = "DirtSimpleP2P ProxyLink Agent Auth v1";
|
||||
|
||||
private ProtocolConstants() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ProtocolException extends IOException {
|
||||
public ProtocolException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProtocolException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package me.proxylink.common;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public record StreamOpenPayload(String routeName) {
|
||||
public StreamOpenPayload {
|
||||
Objects.requireNonNull(routeName, "routeName");
|
||||
if (routeName.isBlank()) {
|
||||
throw new IllegalArgumentException("routeName must not be blank");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encode() throws IOException {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
DataOutputStream output = new DataOutputStream(bytes);
|
||||
output.writeUTF(routeName);
|
||||
output.flush();
|
||||
return bytes.toByteArray();
|
||||
}
|
||||
|
||||
public static StreamOpenPayload decode(byte[] payload) throws IOException {
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(payload));
|
||||
String routeName = input.readUTF();
|
||||
if (input.available() != 0) {
|
||||
throw new ProtocolException("Invalid STREAM_OPEN payload");
|
||||
}
|
||||
return new StreamOpenPayload(routeName);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
artifactId=proxylink-common
|
||||
groupId=me.proxylink
|
||||
version=0.1.0-SNAPSHOT
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
me/proxylink/common/AgentConfig$RouteConfig.class
|
||||
me/proxylink/common/AgentConfig.class
|
||||
me/proxylink/common/AgentConfigIO.class
|
||||
me/proxylink/common/AuthPayloads$Response.class
|
||||
me/proxylink/common/FrameType.class
|
||||
me/proxylink/common/AuthPayloads$Challenge.class
|
||||
me/proxylink/common/ProtocolConstants.class
|
||||
me/proxylink/common/FrameCodec.class
|
||||
me/proxylink/common/AuthPayloads$Hello.class
|
||||
me/proxylink/common/AgentConfig$TunnelConfig.class
|
||||
me/proxylink/common/AgentConfig$Role.class
|
||||
me/proxylink/common/StreamOpenPayload.class
|
||||
me/proxylink/common/AuthPayloads.class
|
||||
me/proxylink/common/AgentConfig$TlsConfig.class
|
||||
me/proxylink/common/ProtocolException.class
|
||||
me/proxylink/common/Frame.class
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AgentConfig.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AgentConfigIO.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/AuthPayloads.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/Frame.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/FrameCodec.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/FrameType.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/ProtocolConstants.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/ProtocolException.java
|
||||
/home/bitnix/Desktop/DirtSimpleP2P/common/src/main/java/me/proxylink/common/StreamOpenPayload.java
|
||||
Binary file not shown.
Reference in New Issue
Block a user