промежуточный коммит

This commit is contained in:
kirillius 2025-09-11 14:20:11 +03:00
parent c9506224ea
commit 08c8fe2f7c
18 changed files with 773 additions and 2 deletions

15
core/pom.xml Normal file
View File

@ -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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version>
</parent>
<artifactId>core</artifactId>
</project>

View File

@ -0,0 +1,72 @@
package ru.kirillius.pf.sdn.core.Auth;
import jakarta.servlet.http.HttpSession;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.core.Util.HashUtil;
import java.util.Objects;
public class AuthManager {
private final Context context;
public final static String SESSION_AUTH_KEY = "auth";
public final static String SESSION_TOKEN = "token";
public AuthManager(Context context) {
this.context = context;
}
public boolean validatePassword(String pass) {
var config = context.getConfig();
return HashUtil.hash(pass, config.getPasswordSalt()).equals(config.getPasswordHash());
}
public void updatePassword(String pass) {
var config = context.getConfig();
config.setPasswordHash(
HashUtil.hash(pass, config.getPasswordSalt())
);
}
public AuthToken createToken(String description) {
var config = context.getConfig();
var token = new AuthToken();
token.setDescripton(description);
config.getTokens().add(token);
config.update();
return token;
}
public boolean validateToken(AuthToken token) {
return context.getConfig().getTokens().contains(token);
}
public void setSessionAuthState(HttpSession session, boolean state) {
session.setAttribute(SESSION_AUTH_KEY, state);
}
public void setSessionToken(HttpSession session, AuthToken token) {
session.setAttribute(SESSION_TOKEN, token);
}
public AuthToken getSessionToken(HttpSession session) {
return (AuthToken) session.getAttribute(SESSION_TOKEN);
}
public boolean getSessionAuthState(HttpSession session) {
return Objects.equals(session.getAttribute(SESSION_AUTH_KEY), Boolean.TRUE);
}
public void invalidateToken(AuthToken token) {
var config = context.getConfig();
config.getTokens().remove(token);
config.update();
}
public boolean validateToken(String token) {
return validateToken(new AuthToken(token));
}
public void invalidateToken(String token) {
invalidateToken(new AuthToken(token));
}
}

View File

@ -0,0 +1,40 @@
package ru.kirillius.pf.sdn.core.Auth;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import java.util.Objects;
import java.util.UUID;
@NoArgsConstructor
@JSONSerializable
public class AuthToken {
public AuthToken(String token) {
this.token = token;
}
@Setter
@Getter
@JSONProperty
private String descripton = "untitled";
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
AuthToken authToken = (AuthToken) o;
return Objects.equals(token, authToken.token);
}
@Override
public int hashCode() {
return Objects.hashCode(token);
}
@Setter
@Getter
@JSONProperty
private String token = UUID.randomUUID().toString();
}

View File

@ -0,0 +1,84 @@
package ru.kirillius.pf.sdn.core;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.json.JSONObject;
import org.json.JSONTokener;
import ru.kirillius.json.JSONArrayProperty;
import ru.kirillius.json.JSONProperty;
import ru.kirillius.json.JSONSerializable;
import ru.kirillius.json.JSONUtility;
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
import java.io.*;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@NoArgsConstructor
@JSONSerializable
public class Config {
@Getter
private File loadedConfigFile = null;
@Setter
@Getter
@JSONProperty
private String host = "0.0.0.0";
@Setter
@Getter
@JSONArrayProperty(type = AuthToken.class)
private List<AuthToken> tokens = Collections.emptyList();
@Setter
@Getter
@JSONProperty
private String passwordSalt = UUID.randomUUID().toString();
@Setter
@Getter
@JSONProperty
private String passwordHash = null;
@Setter
@Getter
@JSONProperty
private int httpPort = 8081;
public void update() {
try {
store(this, loadedConfigFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void store(Config config, File file) throws IOException {
try (var fileInputStream = new FileOutputStream(file)) {
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
writer.write(serialize(config).toString());
writer.flush();
}
}
}
public static JSONObject serialize(Config config) {
return JSONUtility.serializeStructure(config);
}
public static Config deserialize(JSONObject object) {
return JSONUtility.deserializeStructure(object, Config.class);
}
public static Config load(File file) throws IOException {
try (var stream = new FileInputStream(file)) {
var json = new JSONObject(new JSONTokener(stream));
var config = deserialize(json);
config.loadedConfigFile = file;
return config;
}
}
}

View File

@ -0,0 +1,13 @@
package ru.kirillius.pf.sdn.core;
import org.eclipse.jetty.server.Server;
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
public interface Context {
Config getConfig();
AuthManager getAuthManager();
Server getServer();
}

View File

@ -0,0 +1,56 @@
package ru.kirillius.pf.sdn.core.Networking;
import lombok.Getter;
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
import java.util.regex.Pattern;
public class IPv4Subnet {
private final long address;
@Getter
private final int prefixLength;
public IPv4Subnet(String subnet) {
var split = subnet.split(Pattern.quote("/"));
if (split.length != 2) {
throw new IllegalArgumentException("Invalid subnet: " + subnet);
}
var prefix = Integer.parseInt(split[1]);
IPv4Util.validatePrefix(prefix);
address = IPv4Util.ipAddressToLong(split[0]);
prefixLength = prefix;
}
public IPv4Subnet(String address, int prefixLength) {
IPv4Util.validatePrefix(prefixLength);
this.address = IPv4Util.ipAddressToLong(address);
this.prefixLength = prefixLength;
}
public String getAddress() {
return IPv4Util.longToIpAddress(address);
}
@Override
public String toString() {
return getAddress() + '/' + prefixLength;
}
public boolean overlaps(IPv4Subnet subnet) {
var minPrefixLength = Math.min(prefixLength, subnet.prefixLength);
var commonMask = IPv4Util.calculateMask(minPrefixLength);
if (commonMask != prefixLength) {
return false; //can't overlap larger prefix
}
return (address & commonMask) == (subnet.address & commonMask);
}
}

View File

@ -0,0 +1,5 @@
package ru.kirillius.pf.sdn.core.Util;
public class BGPUtility {
}

View File

@ -0,0 +1,30 @@
package ru.kirillius.pf.sdn.core.Util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class HashUtil {
private HashUtil() {
}
public static String hash(String data, String salt) {
String generatedPassword = null;
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-512");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
md.update(salt.getBytes(StandardCharsets.UTF_8));
var bytes = md.digest(data.getBytes(StandardCharsets.UTF_8));
var sb = new StringBuilder();
for (byte aByte : bytes) {
sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
}
generatedPassword = sb.toString();
return generatedPassword;
}
}

View File

@ -0,0 +1,53 @@
package ru.kirillius.pf.sdn.core.Util;
import lombok.SneakyThrows;
import java.net.InetAddress;
import java.util.regex.Pattern;
public class IPv4Util {
private IPv4Util() {
}
private static final Pattern pattern = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
public static void validateAddress(String address) {
if (!pattern.matcher(address).matches()) {
throw new IllegalArgumentException("Invalid IPv4 address: " + address);
}
}
public static void validatePrefix(int prefix) {
if (prefix < 0 || prefix > 32) {
throw new IllegalArgumentException("Invalid IPv4 prefix: " + prefix);
}
}
@SneakyThrows
public static long ipAddressToLong(String address) {
validateAddress(address);
var ip = InetAddress.getByName(address);
var bytes = ip.getAddress();
var result = 0L;
for (var b : bytes) {
result = (result << 8) | (b & 0xFF);
}
return result;
}
public static long calculateMask(int prefixLength) {
validatePrefix(prefixLength);
return 0xFFFFFFFFL << (32 - prefixLength);
}
public static String longToIpAddress(long ipLong) {
if (ipLong < 0 || ipLong > 0xFFFFFFFFL) {
throw new IllegalArgumentException("Address number should be in range 0 - 4294967295");
}
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
}
}

View File

@ -0,0 +1,77 @@
package ru.kirillius.pf.sdn.core.Networking;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class IPv4SubnetTest {
@SuppressWarnings("CatchMayIgnoreException")
@Test
void testInit() {
var validPrefixes = List.of(0, 5, 20, 32);
var invalidPrefixes = List.of(-1, 33, 800);
var validAddresses = List.of("1.2.3.4", "0.0.0.0", "255.255.255.255");
var invalidAddresses = List.of("1.2.3.04", "0.0.0", "255.255.255.255.255", "1.2.3.256", "-1.0.0.0");
validPrefixes.forEach(prefix -> {
validAddresses.forEach(address -> {
var subnet = new IPv4Subnet(address, prefix);
});
});
invalidPrefixes.forEach(prefix -> {
validAddresses.forEach(address -> {
try {
var subnet = new IPv4Subnet(address, prefix);
throw new AssertionError();
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
});
});
validPrefixes.forEach(prefix -> {
invalidAddresses.forEach(address -> {
try {
var subnet = new IPv4Subnet(address, prefix);
throw new AssertionError();
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
});
});
invalidPrefixes.forEach(prefix -> {
invalidAddresses.forEach(address -> {
try {
var subnet = new IPv4Subnet(address, prefix);
throw new AssertionError();
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
});
});
}
@Test
void overlaps() {
checkOverlaps(
new IPv4Subnet("0.0.0.0", 0),
new IPv4Subnet("8.8.8.8", 32)
);
checkOverlaps(
new IPv4Subnet("192.168.0.0", 24),
new IPv4Subnet("192.168.0.128", 25)
);
}
private void checkOverlaps(IPv4Subnet larger, IPv4Subnet smaller) {
assertThat(larger.overlaps(smaller)).isTrue();
assertThat(smaller.overlaps(larger)).isFalse();
}
}

14
launcher/pom.xml Normal file
View File

@ -0,0 +1,14 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version>
</parent>
<artifactId>launcher</artifactId>
</project>

107
pom.xml
View File

@ -7,11 +7,114 @@
<groupId>ru.kirillius</groupId> <groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId> <artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version> <version>0.1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>web-client</module>
<module>launcher</module>
<module>core</module>
<module>web-server</module>
</modules>
<properties> <properties>
<maven.compiler.source>24</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>24</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
<repositories>
<repository>
<id>kirillius</id>
<name>kirillius</name>
<url>https://repo.kirillius.ru/maven</url>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</releases>
<layout>default</layout>
</repository>
</repositories>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.0-M2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>4.0.0-M1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.snmp4j/snmp4j -->
<dependency>
<groupId>org.snmp4j</groupId>
<artifactId>snmp4j</artifactId>
<version>3.7.7</version>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>icmp4j</artifactId>
<version>1.0.0.0</version>
</dependency>
<dependency>
<groupId>ru.kirillius.util</groupId>
<artifactId>dynamic-types</artifactId>
<version>2.0.0.0</version>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>json-rpc-servlet</artifactId>
<version>2.1.4.0</version>
</dependency>
<dependency>
<groupId>ru.kirillius.utils</groupId>
<artifactId>common-logging</artifactId>
<version>1.3.0.0</version>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>hibernate-commons</artifactId>
<version>2.2.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-server -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project> </project>

16
web-client/pom.xml Normal file
View File

@ -0,0 +1,16 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version>
</parent>
<artifactId>web-client</artifactId>
</project>

23
web-server/pom.xml Normal file
View File

@ -0,0 +1,23 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>pf-sdn</artifactId>
<version>0.1.0.0</version>
</parent>
<artifactId>web-server</artifactId>
<dependencies>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>core</artifactId>
<version>0.1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,91 @@
package ru.kirillius.pf.sdn.web;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.jetbrains.annotations.Nullable;
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.utils.logging.SystemLogger;
import ru.kirillius.pf.sdn.web.RPC.AuthRPC;
import ru.kirillius.pf.sdn.web.RPC.RPC;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.Set;
public class HTTPServer extends Server {
public final static String ANY_HOST = "0.0.0.0";
private final static String LOG_CONTEXT = HTTPServer.class.getSimpleName();
private final static String TOKEN_HEADER = "X-Auth-token";
private final JSONRPCServlet JSONRPC = new JSONRPCServlet();
private String getResourceBase() throws MalformedURLException {
var resourceFile = getClass().getClassLoader().getResource("htdocs/index.html");
return new URL(Objects.requireNonNull(resourceFile).getProtocol(), resourceFile.getHost(), resourceFile.getPath()
.substring(0, resourceFile.getPath().lastIndexOf("/")))
.toString();
}
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(AuthRPC.class);
public HTTPServer(Context appContext, int port, @Nullable String host) throws Exception {
var connector = new ServerConnector(this);
connector.setPort(port);
if (host != null && !host.equals(ANY_HOST)) {
connector.setHost(host);
}
this.addConnector(connector);
ServletContextHandler servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
var holder = servletContext.addServlet(DefaultServlet.class, "/");
holder.setInitParameter("resourceBase", getResourceBase());
this.setHandler(servletContext);
start();
JSONRPC.addRequestHandler((request, response, call) -> {
var authManager = appContext.getAuthManager();
var authorized = authManager.getSessionAuthState(call.getContext().getSession());
// Thread.sleep(100);//FIXME remove! debug only
//auth by token
if (!authorized) {
var headerToken = request.getHeader(TOKEN_HEADER);
if (headerToken != null) {
authorized = authManager.validateToken(headerToken);
authManager.setSessionAuthState(call.getContext().getSession(), authorized);
}
}
var isProtectedAccess = call.getMethod().getAnnotation(ProtectedMethod.class);
if (isProtectedAccess != null) {
if (!authorized) throw new SecurityException("Forbidden");
}
});
for (var handlerClass : RPCHandlerTypes) {
var instance = RPC.instantiate(handlerClass, appContext);
//noinspection unchecked
JSONRPC.addTargetInstance((Class<? super RPC>) handlerClass, instance);
}
JSONRPC.getErrorHandler().add(throwable -> {
SystemLogger.error("JRPC Request " +
(throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
" has failed with error", LOG_CONTEXT, throwable.getError());
});
}
}

View File

@ -0,0 +1,11 @@
package ru.kirillius.pf.sdn.web;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ProtectedMethod {
}

View File

@ -0,0 +1,52 @@
package ru.kirillius.pf.sdn.web.RPC;
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
import ru.kirillius.json.rpc.Annotations.JRPCContext;
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
import ru.kirillius.json.rpc.Servlet.CallContext;
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
import ru.kirillius.pf.sdn.core.Context;
import ru.kirillius.pf.sdn.web.ProtectedMethod;
public class AuthRPC implements RPC {
private final Context context;
public AuthRPC(Context context) {
this.context = context;
}
@ProtectedMethod
@JRPCMethod
public AuthToken getAuthToken(@JRPCContext CallContext call) {
return context.getAuthManager().getSessionToken(call.getSession());
}
@ProtectedMethod
@JRPCMethod
public AuthToken rememberCurrentUser(@JRPCContext CallContext call) {
var UA = call.getRequest().getHeader("User-Agent");
if (UA == null) {
UA = "Unknown user agent";
}
var authManager = context.getAuthManager();
var token = authManager.createToken(UA);
authManager.setSessionToken(call.getSession(), token);
return token;
}
@JRPCMethod
public boolean auth(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) {
var authManager = context.getAuthManager();
if (authManager.validatePassword(password)) {
authManager.setSessionAuthState(call.getSession(), true);
}
return false;
}
@JRPCMethod
public void logout(@JRPCContext CallContext call) {
var authManager = context.getAuthManager();
authManager.setSessionAuthState(call.getSession(), false);
}
}

View File

@ -0,0 +1,16 @@
package ru.kirillius.pf.sdn.web.RPC;
import ru.kirillius.pf.sdn.core.Context;
import java.lang.reflect.InvocationTargetException;
public interface RPC {
static <T extends RPC> T instantiate(Class<T> type, Context context) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}