This commit is contained in:
2025-10-02 16:20:55 -04:00
commit 03424166e3
8 changed files with 484 additions and 0 deletions

83
pom.xml Normal file
View File

@@ -0,0 +1,83 @@
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.astronand</groupId>
<artifactId>ast-ip</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jetty.version>11.0.26</jetty.version>
<jakarta.websocket.version>2.1.1</jakarta.websocket.version>
<jakarta.servlet.version>6.0.0</jakarta.servlet.version>
<slf4j.version>2.0.13</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.6.0</version> <!-- Use the latest stable version -->
</dependency>
<!-- Jetty server core -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Jetty WebSocket support (Jakarta WebSocket) -->
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jakarta-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Jakarta WebSocket API -->
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>${jakarta.websocket.version}</version>
</dependency>
<!-- Jakarta Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${jakarta.servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- SLF4J logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler plugin -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,67 @@
package dev.astronand;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Dns extends WebSocketClient {
public Dns(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("DNS: Opened connection to " + getURI());
}
private static String toIP(byte[] bytes) {
if (bytes.length != 4) return "?.?.?.?";
return (bytes[0] & 0xFF) + "." +
(bytes[1] & 0xFF) + "." +
(bytes[2] & 0xFF) + "." +
(bytes[3] & 0xFF);
}
@Override
public void onMessage(String message) {
var bytes = message.getBytes(StandardCharsets.UTF_8);
var ver = bytes[0];
var sender = Arrays.copyOfRange(bytes, 1, 5);
var receiver = Arrays.copyOfRange(bytes, 5, 9);
var contentb = Arrays.copyOfRange(bytes, 9, bytes.length);
var content = new String(contentb, StandardCharsets.UTF_8);
if (ver!=0) {
send(new Packet((byte)0, toIP(receiver), toIP(sender), ("Invalid ver "+((char)ver)+"|\0").getBytes()).toPacket());
}
if (!toIP(receiver).equals("1.1.1.1")) {
System.out.println("WTF");
return;
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed with exit code " + code + " reason: " + reason);
}
@Override
public void onError(Exception ex) {
System.err.println("An error occurred:" + ex);
}
public static void main(String url) throws URISyntaxException {
// Replace with your WebSocket server URI
Dns client = new Dns(new URI(url+"/connect"));
client.connect();
// You can send more messages here if needed
// client.send("Another message");
}
}

View File

@@ -0,0 +1,7 @@
ip map:
0.0.0.0 - 0.255.255.255 | Local network
1.0.0.0 - 1.255.255.255 | Reserved
2.0.0.0 - 126.255.255.255 | Assignable address space
127.0.0.0 - 127.255.255.255 | Loopback addresses
128.0.0.0 - 255.255.255.254 | Mass random address space
255.255.255.255 - 255.255.255.255 | Broadcast address

View File

@@ -0,0 +1,38 @@
package dev.astronand;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Simple HTTP servlet for the AST-IP server.
* Handles GET requests under /http/*
*/
public class ExternalHTTP extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Set content type
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
// Get request path
String path = req.getPathInfo(); // e.g., /test
if (path == null) path = "/";
// Write response
resp.getWriter().println("✅ ExternalHTTP servlet received a GET request!");
resp.getWriter().println("Path: " + path);
resp.getWriter().println("Query: " + req.getQueryString());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// Optional: handle POST requests
resp.setContentType("text/plain");
resp.setCharacterEncoding("UTF-8");
resp.getWriter().println("✅ ExternalHTTP servlet received a POST request!");
}
}

View File

@@ -0,0 +1,183 @@
package dev.astronand;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
@ServerEndpoint("/connect")
public class IpHandler {
private static final Set<Session> sessions = ConcurrentHashMap.newKeySet();
private static final Map<Session, String> ipMap = new ConcurrentHashMap<>();
private static final Map<String, Session> reverseIpMap = new ConcurrentHashMap<>();
byte version = 0;
private static final ScheduledExecutorService heartbeatExecutor =
Executors.newSingleThreadScheduledExecutor();
private static final DateTimeFormatter TS_FORMAT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// IP allocation counter (start at 128.0.0.0)
private static final AtomicLong ipCounter = new AtomicLong(ipToLong(128, 0, 0, 0));
static {
// Schedule heartbeat every 20 seconds
heartbeatExecutor.scheduleAtFixedRate(() -> {
for (Session session : sessions) {
if (session.isOpen()) {
try {
session.getAsyncRemote().sendPing(ByteBuffer.wrap(new byte[]{1}));
} catch (Exception e) {
log("❌ Failed to send heartbeat: " + e.getMessage());
}
}
}
}, 20, 20, TimeUnit.SECONDS);
}
private static void log(String msg) {
String ts = LocalDateTime.now().format(TS_FORMAT);
System.out.println("[" + ts + "] " + msg);
}
private static String toIP(byte[] bytes) {
if (bytes.length != 4) return "?.?.?.?";
return (bytes[0] & 0xFF) + "." +
(bytes[1] & 0xFF) + "." +
(bytes[2] & 0xFF) + "." +
(bytes[3] & 0xFF);
}
private static byte[] ipToBytes(String ip) {
String[] parts = ip.split("\\.");
return new byte[]{
(byte) Integer.parseInt(parts[0]),
(byte) Integer.parseInt(parts[1]),
(byte) Integer.parseInt(parts[2]),
(byte) Integer.parseInt(parts[3])
};
}
private static long ipToLong(int a, int b, int c, int d) {
return ((long) a << 24) | ((long) b << 16) | ((long) c << 8) | d;
}
private static String longToIp(long value) {
return String.format("%d.%d.%d.%d",
(value >> 24) & 0xFF,
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF);
}
private static synchronized String allocateIp() {
long ipLong = ipCounter.getAndIncrement();
if (ipLong > ipToLong(255, 255, 255, 254)) {
throw new RuntimeException("IP pool exhausted");
}
return longToIp(ipLong);
}
@OnOpen
public void onOpen(Session session) {
sessions.add(session);
String assignedIp = allocateIp();
ipMap.put(session, assignedIp);
reverseIpMap.put(assignedIp, session);
log("✅ Client connected: " + session.getId() + " assigned IP " + assignedIp);
try {
var packet = new Packet(version, "1.0.0.0", assignedIp, ipToBytes(assignedIp));
session.getBasicRemote().sendBinary(packet.toPacket());
} catch (IOException e) {
log("⚠ Failed to send IP assignment: " + e.getMessage());
}
}
@OnClose
public void onClose(Session session) {
sessions.remove(session);
String ip = ipMap.remove(session);
if (ip != null) reverseIpMap.remove(ip);
log("❎ Client disconnected: " + session.getId() + " (IP " + ip + ")");
}
@OnMessage
public void onTextMessage(Session session, String message) {
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
parseAndRoute(session, bytes);
}
@OnMessage
public void onBinaryMessage(Session session, byte[] bytes) {
parseAndRoute(session, bytes);
}
private void parseAndRoute(Session session, byte[] bytes) {
try {
var ver = bytes[0];
var senderb = Arrays.copyOfRange(bytes, 1, 5);
var receiverb = Arrays.copyOfRange(bytes, 5, 9);
var contentb = Arrays.copyOfRange(bytes, 9, bytes.length);
var sender = toIP(senderb);
var receiver = toIP(receiverb);
var content = new String(contentb, StandardCharsets.UTF_8);
//if (bytes[0]!=version) {
// //session.getBasicRemote().sendBinary(new Packet((byte) 0, "1.0.0.0", sender, ("Invalid version "+((char)ver)+"|"+((char)version)).getBytes(StandardCharsets.UTF_8)).toPacket().flip());
// return;
//}
log("📦 Packet: " + sender + " > " + receiver + "\n" + content);
// Find target client
Session target = reverseIpMap.get(receiver);
if (target != null && target.isOpen()) {
if (receiver.equals("1.0.0.0")) {
server(session, bytes);
} else {
// Forward the original binary data (preserving header + content)
target.getBasicRemote().sendBinary(ByteBuffer.wrap(bytes));
}
} else {
log("📦 Packet: 1.0.0.0 > " + sender + "\nReceiver "+receiver+" not found");
session.getBasicRemote().sendBinary(new Packet(version, "1.0.0.0", sender, ("Receiver " + receiver + " not found").getBytes(StandardCharsets.UTF_8)).toPacket());
}
} catch (IOException e) {
e.printStackTrace();
try { session.close(); } catch (IOException ignore) {}
}
}
private void server(Session session, byte[] bytes) throws IOException {
var ver = bytes[0];
var senderb = Arrays.copyOfRange(bytes, 1, 5);
var receiverb = Arrays.copyOfRange(bytes, 5, 9);
var contentb = Arrays.copyOfRange(bytes, 9, bytes.length);
var sender = toIP(senderb);
var receiver = toIP(receiverb);
var content = new String(contentb, StandardCharsets.UTF_8);
if (ver!=version) {
session.getBasicRemote().sendBinary(new Packet(version,"1.0.0.0", sender, "Invalid version".getBytes(StandardCharsets.UTF_8)).toPacket().flip());
}
}
@OnError
public void onError(Session session, Throwable t) {
log("⚠ WebSocket error on " + session.getId() + ": " + t.getMessage());
t.printStackTrace();
}
}

View File

@@ -0,0 +1,69 @@
package dev.astronand;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer;
import java.net.URI;
import java.net.URISyntaxException;
public class Main {
static boolean stop = false;
public static void main(String[] args) throws Exception {
int port = 25616;
String serveraddress = "ws://localhost:25616";
// Create Jetty server with no default connector
Server server = new Server();
var DNS = new Thread(() -> {
try {
Dns.main(serveraddress);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
});
// Add shutdown hook to cleanly stop the server
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutting down server...");
server.setStopTimeout(10000);
try {
server.stop();
Main.stop = true;
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
// Bind to all interfaces so domain traffic is accepted
ServerConnector connector = new ServerConnector(server);
connector.setHost("0.0.0.0"); // listens on all interfaces (public + localhost)
connector.setPort(port);
server.addConnector(connector);
// Servlet context
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
server.setHandler(context);
// Enable WebSocket + register endpoint
JakartaWebSocketServletContainerInitializer.configure(context,
(servletContext, wsContainer) -> wsContainer.addEndpoint(IpHandler.class));
// Register ExternalHTTP servlet
// Map it to any path you like, e.g., /http/*
ServletHolder httpHolder = new ServletHolder(new ExternalHTTP());
context.addServlet(httpHolder, "/net/*");
// Start Jetty
server.start();
System.out.println("✅ AST-IP running on port "+port);
System.out.println("Accepting incoming connections");
DNS.start();
System.out.println("✅ DNS running on ip 1.1.1.1");
server.join();
DNS.join();
}
}

View File

@@ -0,0 +1,28 @@
package dev.astronand;
import java.nio.ByteBuffer;
public record Packet(Byte ver, String sender, String receiver, byte[] content) {
private static byte[] ipToBytes(String ip) {
String[] parts = ip.split("\\.");
return new byte[]{
(byte) Integer.parseInt(parts[0]),
(byte) Integer.parseInt(parts[1]),
(byte) Integer.parseInt(parts[2]),
(byte) Integer.parseInt(parts[3])
};
}
public ByteBuffer toPacket() {
byte[] senderb = ipToBytes(sender);
byte[] receiverb = ipToBytes(receiver);
byte[] contentb = content;
ByteBuffer packet = ByteBuffer.allocate(1+senderb.length + receiverb.length + contentb.length);
packet.put(ver);
packet.put(senderb);
packet.put(receiverb);
packet.put(contentb);
return packet.flip();
}
}

View File

@@ -0,0 +1,9 @@
package dev.astronand;
public record Registry() {
public boolean hasClient(String ip) {
return false;
}
}