Compare commits
57 Commits
classic
...
1.0.1-BETA
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
740e85fa3f | ||
|
|
357b30b037 | ||
|
|
f81cb0ee15 | ||
|
|
09dd207bb1 | ||
|
|
2d829fe341 | ||
|
|
de22a8ab67 | ||
|
|
a365b57638 | ||
|
|
eda1b24a26 | ||
| 0ab292ba72 | |||
|
|
f2c35c1e2c | ||
|
|
64ce55ea7b | ||
|
|
9483c36a66 | ||
|
|
7f2865a5d8 | ||
| 9d2670ebbe | |||
| f978895be8 | |||
| 49764f445a | |||
| 911a166d48 | |||
|
|
3b7301e974 | ||
|
|
39461fc07a | ||
|
|
26c1427dc6 | ||
|
|
ee40b4785a | ||
|
|
3180cde3c9 | ||
|
|
0c21d04488 | ||
|
|
323c256050 | ||
|
|
bf6a26a5c5 | ||
|
|
10aafd1218 | ||
|
|
3b962af0d3 | ||
|
|
fce45566fb | ||
|
|
77eb54afc2 | ||
|
|
c4dac2373f | ||
|
|
76d16209ff | ||
|
|
3848126854 | ||
|
|
7c8efa597b | ||
|
|
5e7f8b7cf3 | ||
|
|
db66ce0a21 | ||
|
|
6589160632 | ||
|
|
63ac0c2d89 | ||
|
|
978d30a82c | ||
|
|
8cd38d18a5 | ||
| 5f4889a3db | |||
| 70375fb286 | |||
|
|
c25d223e32 | ||
|
|
b36f96b9b9 | ||
|
|
f9b53f1192 | ||
|
|
181274d818 | ||
|
|
c7ca67176d | ||
|
|
5fec181a74 | ||
|
|
887587f5de | ||
|
|
f74e7100aa | ||
|
|
17cc3449d2 | ||
|
|
16ea18d95f | ||
|
|
41884d9d39 | ||
|
|
5f1bc8285d | ||
|
|
829a998331 | ||
|
|
509ed5d1bf | ||
|
|
25d6f1de21 | ||
|
|
6f5d355f79 |
6
.idea/GitLink.xml
generated
Normal file
6
.idea/GitLink.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="uk.co.ben_gibson.git.link.SettingsState">
|
||||||
|
<option name="host" value="e0f86390-1091-4871-8aeb-f534fbc99cf0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
70
LICENSE
70
LICENSE
@@ -1,68 +1,2 @@
|
|||||||
Open Autonomous Public License (OAPL) v1.0
|
Please read the license here: https://open-autonomous-connection.org/license.html
|
||||||
Copyright (C) 2024-2025 Open Autonomous Connection (OAC)
|
Download all third parties licenses here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||||
Projekt-URL: https://open-autonomous-connection.org/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
1. Nutzungsrechte
|
|
||||||
Diese Software darf sowohl für private als auch kommerzielle Zwecke genutzt werden. Die Nutzung ist unter den Bedingungen dieser Lizenz gestattet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
2. Verkaufsverbot
|
|
||||||
Es ist nicht gestattet, diese Software oder abgeleitete Werke davon zu verkaufen oder kommerziell zu vertreiben.
|
|
||||||
Dies umfasst auch jede Form der direkten oder indirekten Monetarisierung der Software selbst.
|
|
||||||
Es ist gestattet, die Software im Rahmen von Dienstleistungen kommerziell zu nutzen, solange der Quellcode und die Originaldateien kostenlos verfügbar bleiben und nicht gegen Entgelt verkauft oder monetarisiert werden.
|
|
||||||
Jede Form der Monetarisierung der Software selbst, wie der Verkauf oder die Lizenzierung der Software, ist untersagt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
3. Offenlegung des Quellcodes
|
|
||||||
Die Software ist dauerhaft quelloffen. Der vollständige Quellcode muss bei jeder Verbreitung, auch in geänderter Form, mitgeliefert oder öffentlich zugänglich gemacht werden.
|
|
||||||
Jede Version, auch veränderte, muss einen klar sichtbaren Verweis auf das Originalprojekt enthalten:
|
|
||||||
→ https://github.com/Open-Autonomous-Connection/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
4. Weitergabe & Lizenzvererbung
|
|
||||||
Die Software darf frei kopiert, verteilt und verändert werden, unter folgenden Bedingungen:
|
|
||||||
|
|
||||||
- Die Original-Lizenz (OAPL v1.0) muss vollständig und unverändert mitgeliefert werden.
|
|
||||||
- Alle Änderungen am Quellcode müssen klar kenntlich gemacht werden (z.B. durch Kommentare oder Änderungsprotokolle).
|
|
||||||
- Abgeleitete Werke müssen unter derselben Lizenz (OAPL v1.0) veröffentlicht werden, es sei denn, die Lizenzierung erfolgt in einem Kontext,
|
|
||||||
in dem dies durch geltendes Recht oder technische Einschränkungen nicht möglich ist. In diesem Fall muss der Quellcode der Änderungen weiterhin offen und zugänglich gemacht werden.
|
|
||||||
- Es dürfen keine zusätzlichen Einschränkungen oder Bedingungen auferlegt werden, die den Bedingungen dieser Lizenz widersprechen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
5. Keine proprietäre Nutzung
|
|
||||||
Die Software oder ihre abgeleiteten Werke dürfen nicht in proprietäre Software integriert oder unter einer Lizenz weitergegeben werden, die die Bedingungen dieser Lizenz einschränkt oder die Offenlegung des Quellcodes unterlässt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
6. Keine Veränderung dieser Lizenz
|
|
||||||
Diese Lizenz darf nicht verändert oder durch andere Lizenzen ersetzt werden. Eine Modifikation oder Re-Lizenzierung ist ausdrücklich untersagt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
7. Haftungsausschluss ("as-is")
|
|
||||||
DIE SOFTWARE WIRD 'WIE BESEHEN' BEREITGESTELLT, OHNE AUSDRÜCKLICHE ODER IMPLIZIERTE GARANTIEN, EINSCHLIESSLICH, ABER NICHT BESCHRÄNKT AUF GARANTIEN DER MARKTGÄNGIGKEIT,
|
|
||||||
DER EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG VON RECHTEN DRITTER. SOWEIT ES GESCHÄFTSRECHTLICH ZULÄSSIG IST,
|
|
||||||
WIRD DIE HAFTUNG DER AUTOREN FÜR SCHÄDEN ODER VERLUSTE AUFGRUND DER NUTZUNG DER SOFTWARE AUSGESCHLOSSEN.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
8. Salvatorische Klausel
|
|
||||||
Sollte eine Bestimmung dieser Lizenz als unwirksam, undurchsetzbar oder nicht durchsetzbar erklärt werden,
|
|
||||||
bleibt die Gültigkeit der übrigen Bestimmungen davon unberührt. In diesem Fall wird die unwirksame Klausel durch eine wirksame und durchsetzbare Bestimmung ersetzt,
|
|
||||||
die dem ursprünglichen wirtschaftlichen Zweck der unwirksamen Bestimmung am nächsten kommt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Ende der Lizenzbedingungen.
|
|
||||||
|
|
||||||
Additional Notice regarding UnlegitLibrary:
|
|
||||||
UnlegitLibrary is primarily distributed under the GNU GPLv3.
|
|
||||||
For the purposes of the Open Autonomous Connection (OAC) project,
|
|
||||||
the author has also licensed UnlegitLibrary under the Open Autonomous Public License (OAPL).
|
|
||||||
Within OAC, the OAPL terms apply to UnlegitLibrary.
|
|
||||||
|
|||||||
45
README.MD
45
README.MD
@@ -1,51 +1,24 @@
|
|||||||
# Open Autonomous Connection Protocol
|
# Open Autonomous Connection INS
|
||||||
|
|
||||||
This is the Protocol for our Open Autonomous Connection project.<br />
|
This is the INS for our Open Autonomous Connection project.<br />
|
||||||
You can easily implement this Protocol via Maven.<br />
|
|
||||||
Feel free to join our Discord.
|
Feel free to join our Discord.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## License Notice
|
## License Notice
|
||||||
|
|
||||||
This project (OAC) is licensed under the [Open Autonomous Public License (OAPL)](https://repo.open-autonomous-connection.org/open-autonomous-connection/OAPL/).
|
This project (OAC) is licensed under
|
||||||
|
the [Open Autonomous Public License (OAPL)](https://open-autonomous-connection.org/license.html).
|
||||||
|
|
||||||
**Third-party components:**
|
**Third-party components:**
|
||||||
|
<br />
|
||||||
|
Download all license here: https://open-autonomous-connection.org/assets/licenses.zip
|
||||||
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
|
- *UnlegitLibrary* is authored by the same copyright holder and is used here under a special agreement:
|
||||||
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
While [UnlegitLibrary](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/) is generally distributed under
|
||||||
|
the [GNU GPLv3](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master/LICENSE),
|
||||||
it is additionally licensed under OAPL **exclusively for the OAC project**.
|
it is additionally licensed under OAPL **exclusively for the OAC project**.
|
||||||
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.
|
Therefore, within OAC, the OAPL terms apply to UnlegitLibrary as well.
|
||||||
|
|
||||||
# Bugs/Problems
|
### Take a look into [Certificate Generation](https://repo.unlegitdqrk.dev/UnlegitDqrk/unlegitlibrary/src/branch/master#certificate-generation-for-networksystem).
|
||||||
# In progress
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
## Certificate generation for NetworkSystem
|
|
||||||
### Creating Root-CA:
|
|
||||||
````
|
|
||||||
openssl genrsa -out myCA.key 4096
|
|
||||||
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 3650 -out myCA.pem
|
|
||||||
|
|
||||||
myCA.key = private Key for CA (keep secret)
|
|
||||||
myCA.pem = public Root-Certificate for signing server and client certificates
|
|
||||||
````
|
|
||||||
### Creating (DNS-/Web-)Server Certificate based on Root-CA:
|
|
||||||
````
|
|
||||||
openssl genrsa -out server.key 2048
|
|
||||||
openssl req -new -key server.key -out server.csr
|
|
||||||
openssl x509 -req -in server.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out server.crt -days 825 -sha256
|
|
||||||
|
|
||||||
server.key = private Key for Server
|
|
||||||
server.crt = Server-Certificate signed by Root-CA
|
|
||||||
````
|
|
||||||
### Optional: Creating Client Certificate based on Root-CA:
|
|
||||||
````
|
|
||||||
openssl genrsa -out client.key 2048
|
|
||||||
openssl req -new -key client.key -out client.csr
|
|
||||||
openssl x509 -req -in client.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out client.crt -days 825 -sha256
|
|
||||||
|
|
||||||
client.key = private Key for Client
|
|
||||||
client.crt = Client-Certificate signed by Root-CA
|
|
||||||
````
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> All certificate registrars require the Root CA to issue a server/client certificate
|
> All certificate registrars require the Root CA to issue a server/client certificate
|
||||||
216
database.sql
Normal file
216
database.sql
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.1deb1+deb12u1
|
||||||
|
-- https://www.phpmyadmin.net/
|
||||||
|
--
|
||||||
|
-- Host: localhost:3306
|
||||||
|
-- Generation Time: Feb 10, 2026 at 08:12 PM
|
||||||
|
-- Server version: 10.11.14-MariaDB-0+deb12u2
|
||||||
|
-- PHP Version: 8.2.29
|
||||||
|
|
||||||
|
SET
|
||||||
|
SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET
|
||||||
|
time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Database: `oac_ins`
|
||||||
|
--
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `infonames`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `infonames`
|
||||||
|
(
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`info` varchar(63) NOT NULL,
|
||||||
|
`tln_id` int(11) NOT NULL,
|
||||||
|
`uid` int(11) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `records`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `records`
|
||||||
|
(
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`infoname_id` int(11) NOT NULL,
|
||||||
|
`subname_id` int(11) DEFAULT NULL,
|
||||||
|
`type` enum('A','AAAA','TXT','CNAME','MX','SRV','NS') NOT NULL,
|
||||||
|
`value` varchar(2048) NOT NULL,
|
||||||
|
`ttl` int(11) DEFAULT 3600,
|
||||||
|
`priority` int(11) DEFAULT NULL,
|
||||||
|
`port` int(11) DEFAULT NULL,
|
||||||
|
`weight` int(11) DEFAULT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `subnames`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `subnames`
|
||||||
|
(
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`name` varchar(63) NOT NULL,
|
||||||
|
`infoname_id` int(11) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `tln`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `tln`
|
||||||
|
(
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`name` varchar(63) NOT NULL,
|
||||||
|
`info` text DEFAULT NULL,
|
||||||
|
`owner_id` int(11) DEFAULT NULL,
|
||||||
|
`is_public` tinyint(1) DEFAULT 1,
|
||||||
|
`allow_subdomains` tinyint(1) DEFAULT 1
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `users`
|
||||||
|
(
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`uid` varchar(36) NOT NULL,
|
||||||
|
`username` varchar(64) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `infonames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `infonames`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `tln_id` (`tln_id`),
|
||||||
|
ADD KEY `uid` (`uid`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `records`
|
||||||
|
--
|
||||||
|
ALTER TABLE `records`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `infoname_id` (`infoname_id`),
|
||||||
|
ADD KEY `subname_id` (`subname_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `subnames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `subnames`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD KEY `infoname_id` (`infoname_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `tln`
|
||||||
|
--
|
||||||
|
ALTER TABLE `tln`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `name` (`name`),
|
||||||
|
ADD KEY `owner_id` (`owner_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `uid` (`uid`),
|
||||||
|
ADD UNIQUE KEY `username` (`username`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `infonames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `infonames`
|
||||||
|
MODIFY `id` int (11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `records`
|
||||||
|
--
|
||||||
|
ALTER TABLE `records`
|
||||||
|
MODIFY `id` int (11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `subnames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `subnames`
|
||||||
|
MODIFY `id` int (11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `tln`
|
||||||
|
--
|
||||||
|
ALTER TABLE `tln`
|
||||||
|
MODIFY `id` int (11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY `id` int (11) NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `infonames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `infonames`
|
||||||
|
ADD CONSTRAINT `infonames_ibfk_1` FOREIGN KEY (`tln_id`) REFERENCES `tln` (`id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `infonames_ibfk_2` FOREIGN KEY (`uid`) REFERENCES `users` (`id`) ON
|
||||||
|
DELETE
|
||||||
|
CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `records`
|
||||||
|
--
|
||||||
|
ALTER TABLE `records`
|
||||||
|
ADD CONSTRAINT `records_ibfk_1` FOREIGN KEY (`infoname_id`) REFERENCES `infonames` (`id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `records_ibfk_2` FOREIGN KEY (`subname_id`) REFERENCES `subnames` (`id`) ON
|
||||||
|
DELETE
|
||||||
|
CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `subnames`
|
||||||
|
--
|
||||||
|
ALTER TABLE `subnames`
|
||||||
|
ADD CONSTRAINT `subnames_ibfk_1` FOREIGN KEY (`infoname_id`) REFERENCES `infonames` (`id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `tln`
|
||||||
|
--
|
||||||
|
ALTER TABLE `tln`
|
||||||
|
ADD CONSTRAINT `tln_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE SET NULL;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
120
dependency-reduced-pom.xml
Normal file
120
dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?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/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
|
<artifactId>INSServer</artifactId>
|
||||||
|
<version>1.0.1-BETA.0.3</version>
|
||||||
|
<description>The default INS-Server</description>
|
||||||
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
|
<issueManagement>
|
||||||
|
<system>Issue Tracker</system>
|
||||||
|
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/INSServer/issues</url>
|
||||||
|
</issueManagement>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<name>UnlegitDqrk</name>
|
||||||
|
<url>https://unlegitdqrk.dev/</url>
|
||||||
|
<organization>Open Autonomous Connection</organization>
|
||||||
|
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
|
||||||
|
<roles>
|
||||||
|
<role>Owner</role>
|
||||||
|
<role>Head Developer</role>
|
||||||
|
</roles>
|
||||||
|
</developer>
|
||||||
|
<developer>
|
||||||
|
<name>Maple</name>
|
||||||
|
<url>https://niumaple.carrd.co/</url>
|
||||||
|
<organization>Open Autonomous Connection</organization>
|
||||||
|
<organizationUrl>https://open-autonomous-connection.org/</organizationUrl>
|
||||||
|
<roles>
|
||||||
|
<role>Owner</role>
|
||||||
|
<role>Head Developer</role>
|
||||||
|
</roles>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Open Autonomous Public License (OAPL)</name>
|
||||||
|
<url>https://open-autonomous-connection.org/license.html</url>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<organization>
|
||||||
|
<name>Open Autonomous Connection</name>
|
||||||
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
|
</organization>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer>
|
||||||
|
<mainClass>org.openautonomousconnection.insserver.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>--add-exports</arg>
|
||||||
|
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||||
|
<arg>--add-exports</arg>
|
||||||
|
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>3.6.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-javadocs</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<failOnError>false</failOnError>
|
||||||
|
<failOnWarnings>false</failOnWarnings>
|
||||||
|
<doclint>none</doclint>
|
||||||
|
<locale>en_US</locale>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<docencoding>UTF-8</docencoding>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<snapshots />
|
||||||
|
<id>oac</id>
|
||||||
|
<url>https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<properties>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
|
</properties>
|
||||||
|
</project>
|
||||||
389
frontend/dashboard.java
Normal file
389
frontend/dashboard.java
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
package ins.frontend;
|
||||||
|
|
||||||
|
import ins.frontend.utils.RegistrarDao;
|
||||||
|
import ins.frontend.utils.WebApp;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
|
import org.openautonomousconnection.webserver.api.Route;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPage;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||||
|
import org.openautonomousconnection.webserver.utils.HeaderMaps;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Html;
|
||||||
|
import org.openautonomousconnection.webserver.utils.MergedRequestParams;
|
||||||
|
import org.openautonomousconnection.webserver.utils.WebUrlUtil;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INS registrar dashboard (TLN / InfoName / Records) for protocol v1.0.1 resource packets.
|
||||||
|
*
|
||||||
|
* <p>Supported actions (POST recommended for mutations):</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>create_tln</li>
|
||||||
|
* <li>update_tln</li>
|
||||||
|
* <li>delete_tln</li>
|
||||||
|
* <li>create_infoname</li>
|
||||||
|
* <li>delete_infoname</li>
|
||||||
|
* <li>add_record</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Route(path = "/dashboard.html")
|
||||||
|
public final class dashboard implements WebPage {
|
||||||
|
|
||||||
|
private static Integer normalizeNullableInt(String s) {
|
||||||
|
if (s == null) return null;
|
||||||
|
String t = s.trim();
|
||||||
|
if (t.isEmpty()) return null;
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(t);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebResourceResponsePacket handle(WebPageContext ctx) throws Exception {
|
||||||
|
WebApp.init();
|
||||||
|
|
||||||
|
if (ctx.session == null || !ctx.session.isValid() || ctx.session.getUser() == null) {
|
||||||
|
return plain(ctx, 401, "Authentication required (session).");
|
||||||
|
}
|
||||||
|
|
||||||
|
int userId;
|
||||||
|
try {
|
||||||
|
userId = Integer.parseInt(ctx.session.getUser());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return plain(ctx, 401, "Invalid session user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrarDao dao = WebApp.get().dao();
|
||||||
|
|
||||||
|
// Build target for param merging: "/path?query"
|
||||||
|
String rawTarget = WebUrlUtil.extractPathAndQuery(ctx.request.getUrl());
|
||||||
|
if (rawTarget == null) rawTarget = "/dashboard.html";
|
||||||
|
|
||||||
|
Map<String, String> headers = ctx.request.getHeaders();
|
||||||
|
byte[] body = ctx.request.getBody();
|
||||||
|
|
||||||
|
MergedRequestParams p = MergedRequestParams.from(rawTarget, headers, body);
|
||||||
|
|
||||||
|
String msg = null;
|
||||||
|
String err = null;
|
||||||
|
|
||||||
|
String action = p.getOr("action", "").trim();
|
||||||
|
if (!action.isBlank()) {
|
||||||
|
try {
|
||||||
|
ActionResult r = executeAction(action, p, userId, dao);
|
||||||
|
msg = r.msg();
|
||||||
|
err = r.err();
|
||||||
|
} catch (Exception e) {
|
||||||
|
err = "Action failed: " + safeMsg(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(ctx, userId, msg, err, dao);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult executeAction(String action, MergedRequestParams p, int userId, RegistrarDao dao) throws Exception {
|
||||||
|
String a = action.trim().toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
if ("create_tln".equals(a)) {
|
||||||
|
String name = p.get("name");
|
||||||
|
String info = p.getOr("info", "");
|
||||||
|
boolean isPublic = p.getBool("is_public");
|
||||||
|
boolean allowSubs = p.getBool("allow_subdomains");
|
||||||
|
|
||||||
|
if (name == null || name.isBlank()) return ActionResult.err("Missing TLN name.");
|
||||||
|
|
||||||
|
int id = dao.createTln(name.trim(), info, userId, isPublic, allowSubs);
|
||||||
|
return ActionResult.ok("Created TLN id=" + id + " (" + name.trim() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("update_tln".equals(a)) {
|
||||||
|
int id = p.getInt("id", -1);
|
||||||
|
String info = p.getOr("info", "");
|
||||||
|
boolean isPublic = p.getBool("is_public");
|
||||||
|
boolean allowSubs = p.getBool("allow_subdomains");
|
||||||
|
|
||||||
|
if (id <= 0) return ActionResult.err("Invalid TLN id.");
|
||||||
|
|
||||||
|
boolean ok = dao.updateTlnOwned(id, userId, info, isPublic, allowSubs);
|
||||||
|
return ActionResult.ok(ok ? ("Updated TLN id=" + id) : "Not owner / not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("delete_tln".equals(a)) {
|
||||||
|
int id = p.getInt("id", -1);
|
||||||
|
if (id <= 0) return ActionResult.err("Invalid TLN id.");
|
||||||
|
|
||||||
|
boolean ok = dao.deleteTlnOwned(id, userId);
|
||||||
|
return ActionResult.ok(ok ? ("Deleted TLN id=" + id) : "Not owner / not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("create_infoname".equals(a)) {
|
||||||
|
String tlnName = p.get("tln");
|
||||||
|
String info = p.get("info");
|
||||||
|
|
||||||
|
if (tlnName == null || tlnName.isBlank() || info == null || info.isBlank()) {
|
||||||
|
return ActionResult.err("Missing tln / info.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RegistrarDao.TlnRow tln = dao.findTlnByName(tlnName.trim()).orElse(null);
|
||||||
|
if (tln == null) return ActionResult.err("Unknown TLN: " + tlnName);
|
||||||
|
|
||||||
|
if (!RegistrarDao.canUseTln(tln, userId)) {
|
||||||
|
return ActionResult.err("TLN not public and not owned by you.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = dao.createInfoName(tln, info.trim(), userId);
|
||||||
|
return ActionResult.ok("Created infoname id=" + id + " (" + info.trim() + "." + tln.name() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("delete_infoname".equals(a)) {
|
||||||
|
int id = p.getInt("id", -1);
|
||||||
|
if (id <= 0) return ActionResult.err("Invalid infoname id.");
|
||||||
|
|
||||||
|
boolean ok = dao.deleteInfoNameOwned(id, userId);
|
||||||
|
return ActionResult.ok(ok ? ("Deleted infoname id=" + id) : "Not owner / not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("add_record".equals(a)) {
|
||||||
|
int infonameId = p.getInt("infoname_id", -1);
|
||||||
|
String sub = p.get("sub");
|
||||||
|
String type = p.getOr("type", "").trim().toUpperCase(Locale.ROOT);
|
||||||
|
String value = p.get("value");
|
||||||
|
|
||||||
|
int ttl = p.getInt("ttl", 3600);
|
||||||
|
|
||||||
|
Integer priority = normalizeNullableInt(p.get("priority"));
|
||||||
|
Integer port = normalizeNullableInt(p.get("port"));
|
||||||
|
Integer weight = normalizeNullableInt(p.get("weight"));
|
||||||
|
|
||||||
|
if (infonameId <= 0) return ActionResult.err("Invalid infoname_id.");
|
||||||
|
if (!dao.isOwnerOfInfoName(infonameId, userId)) return ActionResult.err("Not owner of this infoname.");
|
||||||
|
if (type.isBlank() || value == null || value.isBlank()) return ActionResult.err("Missing type/value.");
|
||||||
|
|
||||||
|
RegistrarDao.InfoNameRow[] owned = dao.listOwnedInfoNames(userId);
|
||||||
|
RegistrarDao.InfoNameRow row = null;
|
||||||
|
for (RegistrarDao.InfoNameRow r : owned) {
|
||||||
|
if (r.id() == infonameId) {
|
||||||
|
row = r;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row == null) return ActionResult.err("Infoname not found in your ownership list.");
|
||||||
|
|
||||||
|
if (!RegistrarDao.canUseSubname(row.tln(), userId, sub)) {
|
||||||
|
return ActionResult.err("Subnames are not allowed for this TLN (allow_subdomains=0) unless you own the TLN.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer subId = dao.ensureSubname(infonameId, sub);
|
||||||
|
int rid = dao.addRecord(infonameId, subId, type, value.trim(), ttl, priority, port, weight);
|
||||||
|
return ActionResult.ok("Added record id=" + rid + " type=" + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionResult.err("Unknown action: " + action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebResourceResponsePacket render(WebPageContext ctx, int userId, String msg, String err, RegistrarDao dao) throws Exception {
|
||||||
|
RegistrarDao.TlnRow[] tlns = dao.listVisibleTlns(userId);
|
||||||
|
RegistrarDao.InfoNameRow[] owned = dao.listOwnedInfoNames(userId);
|
||||||
|
|
||||||
|
String tlnHtml = renderTlnSection(tlns, userId);
|
||||||
|
String infoHtml = renderInfoNamesSection(owned);
|
||||||
|
|
||||||
|
String body = """
|
||||||
|
<div class="card">
|
||||||
|
<h2>INS Registrar</h2>
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
|
||||||
|
<h3>Create TLN</h3>
|
||||||
|
<form method="post" action="dashboard.html" class="form">
|
||||||
|
<input type="hidden" name="action" value="create_tln">
|
||||||
|
<label><span class="muted">name</span><input type="text" name="name" placeholder="com" required></label>
|
||||||
|
<label><span class="muted">info</span><input type="text" name="info" required placeholder="ip[:port]"></label>
|
||||||
|
<label><span class="muted">is_public</span><input type="text" name="is_public" value="1" required></label>
|
||||||
|
<label><span class="muted">allow_subdomains</span><input type="text" name="allow_subdomains" value="1" required></label>
|
||||||
|
<button type="submit">Create TLN</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>TLNs (public + owned)</h3>
|
||||||
|
%s
|
||||||
|
|
||||||
|
<h3>Create InfoName</h3>
|
||||||
|
<p class="muted">Allowed if TLN is public or owned by you.</p>
|
||||||
|
<form method="post" action="dashboard.html" class="form">
|
||||||
|
<input type="hidden" name="action" value="create_infoname">
|
||||||
|
<label><span class="muted">tln</span><input type="text" name="tln" placeholder="com" required></label>
|
||||||
|
<label><span class="muted">info</span><input type="text" name="info" placeholder="example" required></label>
|
||||||
|
<button type="submit">Create InfoName</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>Add Record</h3>
|
||||||
|
<p class="muted">Subname requires allow_subdomains=1 unless you own the TLN. Root (no sub) always allowed.</p>
|
||||||
|
<form method="post" action="dashboard.html" class="form">
|
||||||
|
<input type="hidden" name="action" value="add_record">
|
||||||
|
<label><span class="muted">infoname_id</span><input type="number" name="infoname_id" min="1" required></label>
|
||||||
|
<label><span class="muted">sub (optional)</span><input type="text" name="sub" placeholder="www"></label>
|
||||||
|
<label><span class="muted">type</span><input type="text" name="type" placeholder="A/AAAA/TXT/CNAME/MX/SRV/NS" required></label>
|
||||||
|
<label><span class="muted">value</span><input type="text" name="value" required></label>
|
||||||
|
<label><span class="muted">ttl</span><input type="number" name="ttl" value="3600"></label>
|
||||||
|
<label><span class="muted">priority (MX/SRV)</span><input type="number" name="priority"></label>
|
||||||
|
<label><span class="muted">port (SRV)</span><input type="number" placeholder="Default: 1028" value="1028" name="port"></label>
|
||||||
|
<label><span class="muted">weight (SRV)</span><input type="number" name="weight"></label>
|
||||||
|
<button type="submit">Add Record</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h3>Your InfoNames</h3>
|
||||||
|
%s
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><a href="index.html">Home</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""".formatted(
|
||||||
|
msg == null ? "" : "<p class='ok'>" + Html.esc(msg) + "</p>",
|
||||||
|
err == null ? "" : "<p class='err'>" + Html.esc(err) + "</p>",
|
||||||
|
tlnHtml,
|
||||||
|
infoHtml
|
||||||
|
);
|
||||||
|
|
||||||
|
String html = Html.page("INS Registrar", body);
|
||||||
|
return html(ctx, 200, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderTlnSection(RegistrarDao.TlnRow[] tlns, int userId) {
|
||||||
|
if (tlns == null || tlns.length == 0) {
|
||||||
|
return "<p class='muted'>No TLNs available.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<ul>");
|
||||||
|
for (RegistrarDao.TlnRow t : tlns) {
|
||||||
|
boolean ownedByMe = (t.ownerId() != null && t.ownerId() == userId);
|
||||||
|
|
||||||
|
sb.append("<li>")
|
||||||
|
.append("<code>").append(Html.esc(t.name())).append("</code>")
|
||||||
|
.append(" <span class='muted'>(id=").append(t.id()).append(")</span> ")
|
||||||
|
.append(ownedByMe ? "<span class='muted'>(owner)</span>" : "<span class='muted'>(public)</span>")
|
||||||
|
.append(" <span class='muted'>is_public=").append(t.isPublic() ? "1" : "0")
|
||||||
|
.append(", allow_subdomains=").append(t.allowSubdomains() ? "1" : "0")
|
||||||
|
.append("</span>");
|
||||||
|
|
||||||
|
if (ownedByMe) {
|
||||||
|
sb.append("""
|
||||||
|
<div class="row" style="margin-top:8px; gap:10px; flex-wrap:wrap;">
|
||||||
|
<form method="post" action="dashboard.html" class="form" style="margin:0;">
|
||||||
|
<input type="hidden" name="action" value="update_tln">
|
||||||
|
<input type="hidden" name="id" value="%d">
|
||||||
|
<label>
|
||||||
|
<span class="muted">info</span>
|
||||||
|
<input type="text" name="info" value="%s">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span class="muted">is_public</span>
|
||||||
|
<input type="text" name="is_public" value="%s" style="width:70px;">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span class="muted">allow_subdomains</span>
|
||||||
|
<input type="text" name="allow_subdomains" value="%s" style="width:70px;">
|
||||||
|
</label>
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="dashboard.html" style="margin:0;">
|
||||||
|
<input type="hidden" name="action" value="delete_tln">
|
||||||
|
<input type="hidden" name="id" value="%d">
|
||||||
|
<button type="submit" class="muted">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
""".formatted(
|
||||||
|
t.id(),
|
||||||
|
Html.esc(t.info() == null ? "" : t.info()),
|
||||||
|
t.isPublic() ? "1" : "0",
|
||||||
|
t.allowSubdomains() ? "1" : "0",
|
||||||
|
t.id()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("</li>");
|
||||||
|
}
|
||||||
|
sb.append("</ul>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderInfoNamesSection(RegistrarDao.InfoNameRow[] owned) {
|
||||||
|
if (owned == null || owned.length == 0) {
|
||||||
|
return "<p class='muted'>No infonames yet.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<ul>");
|
||||||
|
for (RegistrarDao.InfoNameRow r : owned) {
|
||||||
|
sb.append("<li>")
|
||||||
|
.append("<code>")
|
||||||
|
.append(Html.esc(r.info())).append(".").append(Html.esc(r.tln().name()))
|
||||||
|
.append("</code>")
|
||||||
|
.append(" <span class='muted'>(id=").append(r.id()).append(")</span>")
|
||||||
|
.append("""
|
||||||
|
<form method="post" action="dashboard.html" style="display:inline; margin-left:10px;">
|
||||||
|
<input type="hidden" name="action" value="delete_infoname">
|
||||||
|
<input type="hidden" name="id" value="%d">
|
||||||
|
<button type="submit" class="muted">Delete</button>
|
||||||
|
</form>
|
||||||
|
""".formatted(r.id()))
|
||||||
|
.append("</li>");
|
||||||
|
}
|
||||||
|
sb.append("</ul>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebResourceResponsePacket plain(WebPageContext ctx, int code, String text) {
|
||||||
|
byte[] body = (text == null ? "" : text).getBytes(StandardCharsets.UTF_8);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), code, "text/plain; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebResourceResponsePacket html(WebPageContext ctx, int code, String html) {
|
||||||
|
byte[] body = Html.utf8(html);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), code, "text/html; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebPacketHeader outHeader(WebPageContext ctx) {
|
||||||
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
||||||
|
if (in == null) {
|
||||||
|
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return new WebPacketHeader(
|
||||||
|
in.getRequestId(),
|
||||||
|
in.getTabId(),
|
||||||
|
in.getPageId(),
|
||||||
|
in.getFrameId(),
|
||||||
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeMsg(Exception e) {
|
||||||
|
String m = e.getMessage();
|
||||||
|
if (m == null || m.isBlank()) return e.getClass().getSimpleName();
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActionResult(String msg, String err) {
|
||||||
|
static ActionResult ok(String msg) {
|
||||||
|
return new ActionResult(msg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ActionResult err(String err) {
|
||||||
|
return new ActionResult(null, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
frontend/index.java
Normal file
59
frontend/index.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package ins.frontend;
|
||||||
|
|
||||||
|
import ins.frontend.utils.WebApp;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
|
import org.openautonomousconnection.webserver.api.Route;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPage;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||||
|
import org.openautonomousconnection.webserver.utils.HeaderMaps;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Html;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing page for INS registrar frontend (v1.0.1).
|
||||||
|
*/
|
||||||
|
@Route(path = "/index.html")
|
||||||
|
public final class index implements WebPage {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebResourceResponsePacket handle(WebPageContext ctx) {
|
||||||
|
WebApp.init();
|
||||||
|
|
||||||
|
String html = Html.page("OAC INS Registrar", """
|
||||||
|
<div class="card">
|
||||||
|
<h2>OAC INS Registrar</h2>
|
||||||
|
<p class="muted">What you want to do?</p>
|
||||||
|
<div class="col"><a href="info.html">Info</a></div><br />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><a href="login.html">Login</a></div>
|
||||||
|
<div class="col"><a href="register.html">Register</a></div>
|
||||||
|
<div class="col"><a href="dashboard.html">Dashboard</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""");
|
||||||
|
|
||||||
|
byte[] body = Html.utf8(html);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 200, "text/html; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebPacketHeader outHeader(WebPageContext ctx) {
|
||||||
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
||||||
|
if (in == null) {
|
||||||
|
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return new WebPacketHeader(
|
||||||
|
in.getRequestId(),
|
||||||
|
in.getTabId(),
|
||||||
|
in.getPageId(),
|
||||||
|
in.getFrameId(),
|
||||||
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
frontend/info.java
Normal file
73
frontend/info.java
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package ins.frontend;
|
||||||
|
|
||||||
|
import ins.frontend.utils.WebApp;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
|
import org.openautonomousconnection.webserver.api.Route;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPage;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||||
|
import org.openautonomousconnection.webserver.utils.HeaderMaps;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Html;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info page for INS registrar frontend (v1.0.1).
|
||||||
|
*/
|
||||||
|
@Route(path = "/info.html")
|
||||||
|
public final class info implements WebPage {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebResourceResponsePacket handle(WebPageContext ctx) {
|
||||||
|
WebApp.init();
|
||||||
|
|
||||||
|
String html = Html.page("INS Info", """
|
||||||
|
<section class="card">
|
||||||
|
<h2>OAC Default INS Server</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The <strong>Default INS Server</strong> is the official, project-operated
|
||||||
|
name service of the Open Autonomous Connection (OAC) network.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
It provides a trusted reference point for resolving InfoNames
|
||||||
|
and enables initial client connections
|
||||||
|
to the OAC ecosystem.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This server is maintained by the OAC project and is intended
|
||||||
|
as the recommended entry point for public services and new clients.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="muted">
|
||||||
|
Note: Alternative or private INS servers may exist, but the default INS
|
||||||
|
server represents the official and stable reference instance.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
""");
|
||||||
|
|
||||||
|
byte[] body = Html.utf8(html);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 200, "text/html; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebPacketHeader outHeader(WebPageContext ctx) {
|
||||||
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
||||||
|
if (in == null) {
|
||||||
|
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return new WebPacketHeader(
|
||||||
|
in.getRequestId(),
|
||||||
|
in.getTabId(),
|
||||||
|
in.getPageId(),
|
||||||
|
in.getFrameId(),
|
||||||
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
266
frontend/login.java
Normal file
266
frontend/login.java
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
package ins.frontend;
|
||||||
|
|
||||||
|
import ins.frontend.utils.UserDao;
|
||||||
|
import ins.frontend.utils.WebApp;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||||
|
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||||
|
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
|
import org.openautonomousconnection.webserver.api.Route;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPage;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||||
|
import org.openautonomousconnection.webserver.utils.HeaderMaps;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Html;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login page (v1.0.1).
|
||||||
|
*/
|
||||||
|
@Route(path = "/login.html")
|
||||||
|
public final class login implements WebPage {
|
||||||
|
|
||||||
|
private enum ReqMethod { GET, POST, OTHER }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebResourceResponsePacket handle(WebPageContext ctx) throws Exception {
|
||||||
|
WebApp.init();
|
||||||
|
|
||||||
|
// If a valid session already exists -> go dashboard (keep session)
|
||||||
|
if (ctx.session != null && ctx.session.isValid() && ctx.session.getUser() != null) {
|
||||||
|
return redirect302(ctx, "dashboard.html", ctx.session.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ReqMethod method = detectMethod(ctx);
|
||||||
|
if (method == ReqMethod.GET) {
|
||||||
|
return ok(ctx, renderForm(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method != ReqMethod.POST) {
|
||||||
|
return text(ctx, 405, "Method Not Allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = headerIgnoreCase(ctx.request.getHeaders(), "content-type");
|
||||||
|
String ctLower = (contentType == null) ? "" : contentType.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
if (!ctLower.startsWith("application/x-www-form-urlencoded")) {
|
||||||
|
return ok(ctx, renderForm("Unsupported content-type: " + Html.esc(contentType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> form = parseFormUrlEncoded(ctx.request.getBody());
|
||||||
|
String username = first(form, "username");
|
||||||
|
String password = first(form, "password");
|
||||||
|
|
||||||
|
if (username == null || password == null) {
|
||||||
|
return ok(ctx, renderForm("Missing username/password."));
|
||||||
|
}
|
||||||
|
|
||||||
|
String lookupUsername = username.trim();
|
||||||
|
|
||||||
|
UserDao.UserRow user = WebApp.get().users().findByUsername(lookupUsername).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return ok(ctx, renderForm("Invalid credentials."));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean okPw = WebApp.get().passwordHasher().verify(password, user.passwordEncoded());
|
||||||
|
if (!okPw) {
|
||||||
|
return ok(ctx, renderForm("Invalid credentials."));
|
||||||
|
}
|
||||||
|
|
||||||
|
String ip = resolveIp(ctx);
|
||||||
|
String ua = headerIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
||||||
|
if (ua == null) ua = "";
|
||||||
|
|
||||||
|
String session = SessionManager.create(
|
||||||
|
String.valueOf(user.id()),
|
||||||
|
ip,
|
||||||
|
ua,
|
||||||
|
(ProtocolWebServer) ctx.client.getServer()
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect302(ctx, "dashboard.html", session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReqMethod detectMethod(WebPageContext ctx) {
|
||||||
|
// Preferred: request.getMethod() via reflection (avoids depending on specific enum package).
|
||||||
|
try {
|
||||||
|
Method m = ctx.request.getClass().getMethod("getMethod");
|
||||||
|
Object v = m.invoke(ctx.request);
|
||||||
|
if (v != null) {
|
||||||
|
String s = String.valueOf(v).trim().toUpperCase(Locale.ROOT);
|
||||||
|
if ("GET".equals(s)) return ReqMethod.GET;
|
||||||
|
if ("POST".equals(s)) return ReqMethod.POST;
|
||||||
|
return ReqMethod.OTHER;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Fall back below.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: treat presence of body as POST-like.
|
||||||
|
byte[] body = ctx.request.getBody();
|
||||||
|
if (body != null && body.length > 0) return ReqMethod.POST;
|
||||||
|
return ReqMethod.GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket ok(WebPageContext ctx, String html) {
|
||||||
|
byte[] body = Html.utf8(html);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 200, "text/html; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket text(WebPageContext ctx, int code, String msg) {
|
||||||
|
byte[] body = (msg == null ? "" : msg).getBytes(StandardCharsets.UTF_8);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), code, "text/plain; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket redirect302(WebPageContext ctx, String location, String session) {
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("Location", location);
|
||||||
|
if (session != null && !session.isBlank()) {
|
||||||
|
headers.put("Set-Cookie", "session=" + session + "; Path=/; HttpOnly; SameSite=Lax");
|
||||||
|
// optional fallback for stacks that read a direct header:
|
||||||
|
headers.put("session", session);
|
||||||
|
}
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 302, "text/plain; charset=utf-8", headers, new byte[0], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String renderForm(String errOrOk) {
|
||||||
|
String line = "";
|
||||||
|
if (errOrOk != null && !errOrOk.isBlank()) {
|
||||||
|
boolean ok = errOrOk.startsWith("OK:");
|
||||||
|
line = "<p class='" + (ok ? "ok" : "err") + "'>" + errOrOk + "</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
String body = """
|
||||||
|
<div class="card">
|
||||||
|
<h2>Login</h2>
|
||||||
|
%s
|
||||||
|
<form method="post" action="login.html" class="form">
|
||||||
|
<label><span class="muted">Username</span><input type="text" name="username" autocomplete="username" required></label>
|
||||||
|
<label><span class="muted">Password</span><input type="password" name="password" autocomplete="current-password" required></label>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<div class="row" style="margin-top: 12px;">
|
||||||
|
<div class="col"><a href="register.html">Register</a></div>
|
||||||
|
<div class="col"><a href="index.html">Home</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""".formatted(line);
|
||||||
|
|
||||||
|
return Html.page("Login", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveIp(WebPageContext ctx) {
|
||||||
|
if (ctx.client == null || ctx.client.getConnection() == null) return "";
|
||||||
|
if (ctx.client.getConnection().getTcpSocket() == null) return "";
|
||||||
|
if (ctx.client.getConnection().getTcpSocket().getInetAddress() == null) return "";
|
||||||
|
return ctx.client.getConnection().getTcpSocket().getInetAddress().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String headerIgnoreCase(Map<String, String> headers, String key) {
|
||||||
|
if (headers == null || headers.isEmpty() || key == null) return null;
|
||||||
|
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||||
|
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parseFormUrlEncoded(byte[] body) {
|
||||||
|
if (body == null || body.length == 0) return Map.of();
|
||||||
|
|
||||||
|
String raw = new String(body, StandardCharsets.UTF_8);
|
||||||
|
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
int start = 0;
|
||||||
|
while (start <= raw.length()) {
|
||||||
|
int amp = raw.indexOf('&', start);
|
||||||
|
if (amp < 0) amp = raw.length();
|
||||||
|
|
||||||
|
String pair = raw.substring(start, amp);
|
||||||
|
if (!pair.isEmpty()) {
|
||||||
|
int eq = pair.indexOf('=');
|
||||||
|
String k = (eq < 0) ? pair : pair.substring(0, eq);
|
||||||
|
String v = (eq < 0) ? "" : pair.substring(eq + 1);
|
||||||
|
|
||||||
|
String key = decodeFormToken(k);
|
||||||
|
String val = decodeFormToken(v);
|
||||||
|
|
||||||
|
if (!key.isEmpty()) out.computeIfAbsent(key, __ -> new ArrayList<>(1)).add(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
start = amp + 1;
|
||||||
|
if (amp == raw.length()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> frozen = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, List<String>> e : out.entrySet()) frozen.put(e.getKey(), List.copyOf(e.getValue()));
|
||||||
|
return Map.copyOf(frozen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String first(Map<String, List<String>> params, String key) {
|
||||||
|
if (params == null || key == null) return null;
|
||||||
|
List<String> v = params.get(key);
|
||||||
|
if (v == null || v.isEmpty()) return null;
|
||||||
|
String t = v.getFirst();
|
||||||
|
if (t == null) return null;
|
||||||
|
String s = t.trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decodeFormToken(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return "";
|
||||||
|
byte[] tmp = new byte[s.length()];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
|
||||||
|
if (c == '+') {
|
||||||
|
tmp[n++] = (byte) ' ';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '%' && i + 2 < s.length()) {
|
||||||
|
int hi = hex(c = s.charAt(i + 1));
|
||||||
|
int lo = hex(s.charAt(i + 2));
|
||||||
|
if (hi >= 0 && lo >= 0) {
|
||||||
|
tmp[n++] = (byte) ((hi << 4) | lo);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] b = String.valueOf(s.charAt(i)).getBytes(StandardCharsets.UTF_8);
|
||||||
|
for (byte bb : b) tmp[n++] = bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(tmp, 0, n, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hex(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||||
|
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebPacketHeader outHeader(WebPageContext ctx) {
|
||||||
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
||||||
|
if (in == null) {
|
||||||
|
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return new WebPacketHeader(
|
||||||
|
in.getRequestId(),
|
||||||
|
in.getTabId(),
|
||||||
|
in.getPageId(),
|
||||||
|
in.getFrameId(),
|
||||||
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
254
frontend/register.java
Normal file
254
frontend/register.java
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package ins.frontend;
|
||||||
|
|
||||||
|
import ins.frontend.utils.RegistrationService;
|
||||||
|
import ins.frontend.utils.WebApp;
|
||||||
|
import org.openautonomousconnection.protocol.packets.v1_0_1.beta.web.impl.resource.WebResourceResponsePacket;
|
||||||
|
import org.openautonomousconnection.protocol.side.web.ProtocolWebServer;
|
||||||
|
import org.openautonomousconnection.protocol.side.web.managers.SessionManager;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketFlags;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_1.beta.WebPacketHeader;
|
||||||
|
import org.openautonomousconnection.webserver.api.Route;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPage;
|
||||||
|
import org.openautonomousconnection.webserver.api.WebPageContext;
|
||||||
|
import org.openautonomousconnection.webserver.utils.HeaderMaps;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Html;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register page (v1.0.1).
|
||||||
|
*/
|
||||||
|
@Route(path = "/register.html")
|
||||||
|
public final class register implements WebPage {
|
||||||
|
|
||||||
|
private enum ReqMethod { GET, POST, OTHER }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebResourceResponsePacket handle(WebPageContext ctx) throws Exception {
|
||||||
|
WebApp.init();
|
||||||
|
|
||||||
|
// If a valid session already exists -> go dashboard (keep session)
|
||||||
|
if (ctx.session != null && ctx.session.isValid() && ctx.session.getUser() != null) {
|
||||||
|
return redirect302(ctx, "dashboard.html", ctx.session.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
ReqMethod method = detectMethod(ctx);
|
||||||
|
if (method == ReqMethod.GET) {
|
||||||
|
return ok(ctx, renderForm(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method != ReqMethod.POST) {
|
||||||
|
return text(ctx, 405, "Method Not Allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = headerIgnoreCase(ctx.request.getHeaders(), "content-type");
|
||||||
|
String ctLower = (contentType == null) ? "" : contentType.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
if (!ctLower.startsWith("application/x-www-form-urlencoded")) {
|
||||||
|
return ok(ctx, renderForm("Unsupported content-type: " + Html.esc(contentType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> form = parseFormUrlEncoded(ctx.request.getBody());
|
||||||
|
String username = first(form, "username");
|
||||||
|
String password = first(form, "password");
|
||||||
|
|
||||||
|
RegistrationService service = new RegistrationService(WebApp.get().users(), WebApp.get().passwordHasher());
|
||||||
|
RegistrationService.Result r = service.register(username, password);
|
||||||
|
|
||||||
|
if (!r.ok()) {
|
||||||
|
return ok(ctx, renderForm(r.error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session (user just registered)
|
||||||
|
String ip = resolveIp(ctx);
|
||||||
|
String ua = headerIgnoreCase(ctx.request.getHeaders(), "user-agent");
|
||||||
|
if (ua == null) ua = "";
|
||||||
|
|
||||||
|
String session = SessionManager.create(
|
||||||
|
String.valueOf(r.userId()),
|
||||||
|
ip,
|
||||||
|
ua,
|
||||||
|
(ProtocolWebServer) ctx.client.getServer()
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect302(ctx, "dashboard.html", session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReqMethod detectMethod(WebPageContext ctx) {
|
||||||
|
try {
|
||||||
|
Method m = ctx.request.getClass().getMethod("getMethod");
|
||||||
|
Object v = m.invoke(ctx.request);
|
||||||
|
if (v != null) {
|
||||||
|
String s = String.valueOf(v).trim().toUpperCase(Locale.ROOT);
|
||||||
|
if ("GET".equals(s)) return ReqMethod.GET;
|
||||||
|
if ("POST".equals(s)) return ReqMethod.POST;
|
||||||
|
return ReqMethod.OTHER;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] body = ctx.request.getBody();
|
||||||
|
if (body != null && body.length > 0) return ReqMethod.POST;
|
||||||
|
return ReqMethod.GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket ok(WebPageContext ctx, String html) {
|
||||||
|
byte[] body = Html.utf8(html);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 200, "text/html; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket text(WebPageContext ctx, int code, String msg) {
|
||||||
|
byte[] body = (msg == null ? "" : msg).getBytes(StandardCharsets.UTF_8);
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("content-length", String.valueOf(body.length));
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), code, "text/plain; charset=utf-8", headers, body, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebResourceResponsePacket redirect302(WebPageContext ctx, String location, String session) {
|
||||||
|
Map<String, String> headers = HeaderMaps.mutable();
|
||||||
|
headers.put("Location", location);
|
||||||
|
if (session != null && !session.isBlank()) {
|
||||||
|
headers.put("Set-Cookie", "session=" + session + "; Path=/; HttpOnly; SameSite=Lax");
|
||||||
|
headers.put("session", session);
|
||||||
|
}
|
||||||
|
return new WebResourceResponsePacket(outHeader(ctx), 302, "text/plain; charset=utf-8", headers, new byte[0], null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String renderForm(String errOrOk) {
|
||||||
|
String line = "";
|
||||||
|
if (errOrOk != null && !errOrOk.isBlank()) {
|
||||||
|
boolean ok = errOrOk.startsWith("OK:");
|
||||||
|
line = "<p class='" + (ok ? "ok" : "err") + "'>" + errOrOk + "</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
String body = """
|
||||||
|
<div class="card">
|
||||||
|
<h2>Register</h2>
|
||||||
|
%s
|
||||||
|
<form method="post" action="register.html" class="form">
|
||||||
|
<label><span class="muted">Username</span><input type="text" name="username" autocomplete="username" required></label>
|
||||||
|
<label><span class="muted">Password</span><input type="password" name="password" autocomplete="new-password" required></label>
|
||||||
|
<button type="submit">Create account</button>
|
||||||
|
</form>
|
||||||
|
<div class="row" style="margin-top: 12px;">
|
||||||
|
<div class="col"><a href="login.html">Login</a></div>
|
||||||
|
<div class="col"><a href="index.html">Home</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""".formatted(line);
|
||||||
|
|
||||||
|
return Html.page("Register", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resolveIp(WebPageContext ctx) {
|
||||||
|
if (ctx.client == null || ctx.client.getConnection() == null) return "";
|
||||||
|
if (ctx.client.getConnection().getTcpSocket() == null) return "";
|
||||||
|
if (ctx.client.getConnection().getTcpSocket().getInetAddress() == null) return "";
|
||||||
|
return ctx.client.getConnection().getTcpSocket().getInetAddress().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String headerIgnoreCase(Map<String, String> headers, String key) {
|
||||||
|
if (headers == null || headers.isEmpty() || key == null) return null;
|
||||||
|
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||||
|
if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) return e.getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> parseFormUrlEncoded(byte[] body) {
|
||||||
|
if (body == null || body.length == 0) return Map.of();
|
||||||
|
|
||||||
|
String raw = new String(body, StandardCharsets.UTF_8);
|
||||||
|
Map<String, List<String>> out = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
int start = 0;
|
||||||
|
while (start <= raw.length()) {
|
||||||
|
int amp = raw.indexOf('&', start);
|
||||||
|
if (amp < 0) amp = raw.length();
|
||||||
|
|
||||||
|
String pair = raw.substring(start, amp);
|
||||||
|
if (!pair.isEmpty()) {
|
||||||
|
int eq = pair.indexOf('=');
|
||||||
|
String k = (eq < 0) ? pair : pair.substring(0, eq);
|
||||||
|
String v = (eq < 0) ? "" : pair.substring(eq + 1);
|
||||||
|
|
||||||
|
String key = decodeFormToken(k);
|
||||||
|
String val = decodeFormToken(v);
|
||||||
|
|
||||||
|
if (!key.isEmpty()) out.computeIfAbsent(key, __ -> new ArrayList<>(1)).add(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
start = amp + 1;
|
||||||
|
if (amp == raw.length()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> frozen = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, List<String>> e : out.entrySet()) frozen.put(e.getKey(), List.copyOf(e.getValue()));
|
||||||
|
return Map.copyOf(frozen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String first(Map<String, List<String>> params, String key) {
|
||||||
|
if (params == null || key == null) return null;
|
||||||
|
List<String> v = params.get(key);
|
||||||
|
if (v == null || v.isEmpty()) return null;
|
||||||
|
String t = v.getFirst();
|
||||||
|
if (t == null) return null;
|
||||||
|
String s = t.trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decodeFormToken(String s) {
|
||||||
|
if (s == null || s.isEmpty()) return "";
|
||||||
|
byte[] tmp = new byte[s.length()];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < s.length(); i++) {
|
||||||
|
char c = s.charAt(i);
|
||||||
|
|
||||||
|
if (c == '+') {
|
||||||
|
tmp[n++] = (byte) ' ';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '%' && i + 2 < s.length()) {
|
||||||
|
int hi = hex(c = s.charAt(i + 1));
|
||||||
|
int lo = hex(s.charAt(i + 2));
|
||||||
|
if (hi >= 0 && lo >= 0) {
|
||||||
|
tmp[n++] = (byte) ((hi << 4) | lo);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] b = String.valueOf(s.charAt(i)).getBytes(StandardCharsets.UTF_8);
|
||||||
|
for (byte bb : b) tmp[n++] = bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(tmp, 0, n, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int hex(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
|
||||||
|
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebPacketHeader outHeader(WebPageContext ctx) {
|
||||||
|
WebPacketHeader in = (ctx != null && ctx.request != null) ? ctx.request.getHeader() : null;
|
||||||
|
if (in == null) {
|
||||||
|
return new WebPacketHeader(0, 0, 0, 0, WebPacketFlags.RESOURCE, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return new WebPacketHeader(
|
||||||
|
in.getRequestId(),
|
||||||
|
in.getTabId(),
|
||||||
|
in.getPageId(),
|
||||||
|
in.getFrameId(),
|
||||||
|
in.getFlags() | WebPacketFlags.RESOURCE,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
490
frontend/utils/RegistrarDao.java
Normal file
490
frontend/utils/RegistrarDao.java
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
package ins.frontend.utils;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registrar DAO for schema:
|
||||||
|
* <ul>
|
||||||
|
* <li>tln(id, name, info, owner_id, is_public, allow_subdomains)</li>
|
||||||
|
* <li>infonames(id, info, tln_id, uid)</li>
|
||||||
|
* <li>subnames(id, name, infoname_id)</li>
|
||||||
|
* <li>records(id, infoname_id, subname_id, type, value, ttl, priority, port, weight)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Rules:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Create InfoName under TLN allowed if TLN is public OR owned by user.</li>
|
||||||
|
* <li>Subnames allowed for non-owner only if allow_subdomains=1. Owner always allowed.</li>
|
||||||
|
* <li>TLN name is not editable; only info/is_public/allow_subdomains are editable (owner-only).</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class RegistrarDao {
|
||||||
|
|
||||||
|
private final UserDao.DataSourceProvider dataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the DAO.
|
||||||
|
*
|
||||||
|
* @param dataSource JDBC connection provider
|
||||||
|
*/
|
||||||
|
public RegistrarDao(UserDao.DataSourceProvider dataSource) {
|
||||||
|
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- TLN ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLN usable for creating InfoNames if public OR owned.
|
||||||
|
*
|
||||||
|
* @param tln TLN row
|
||||||
|
* @param userId current user id
|
||||||
|
* @return allowed
|
||||||
|
*/
|
||||||
|
public static boolean canUseTln(TlnRow tln, int userId) {
|
||||||
|
if (tln == null || userId <= 0) return false;
|
||||||
|
if (tln.isPublic) return true;
|
||||||
|
return tln.ownerId != null && tln.ownerId == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subdomain/subname allowed if:
|
||||||
|
* <ul>
|
||||||
|
* <li>no sub requested (root) OR</li>
|
||||||
|
* <li>TLN allows subdomains OR</li>
|
||||||
|
* <li>user is TLN owner</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param tln TLN row
|
||||||
|
* @param userId current user id
|
||||||
|
* @param sub sub label (nullable)
|
||||||
|
* @return allowed
|
||||||
|
*/
|
||||||
|
public static boolean canUseSubname(TlnRow tln, int userId, String sub) {
|
||||||
|
if (tln == null || userId <= 0) return false;
|
||||||
|
boolean wantsSub = sub != null && !sub.isBlank();
|
||||||
|
if (!wantsSub) return true;
|
||||||
|
if (tln.allowSubdomains) return true;
|
||||||
|
return tln.ownerId != null && tln.ownerId == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TlnRow mapTln(ResultSet rs) throws SQLException {
|
||||||
|
return new TlnRow(
|
||||||
|
rs.getInt("id"),
|
||||||
|
rs.getString("name"),
|
||||||
|
rs.getString("info"),
|
||||||
|
(Integer) rs.getObject("owner_id"),
|
||||||
|
rs.getInt("is_public") == 1,
|
||||||
|
rs.getInt("allow_subdomains") == 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a TLN.
|
||||||
|
*
|
||||||
|
* @param name tln.name (unique)
|
||||||
|
* @param info tln.info (editable, can be null/blank)
|
||||||
|
* @param ownerId owner user id (nullable in DB, pass null to create unowned TLN)
|
||||||
|
* @param isPublic tln.is_public
|
||||||
|
* @param allowSubdomains tln.allow_subdomains
|
||||||
|
* @return new tln.id
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public int createTln(String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) throws SQLException {
|
||||||
|
if (name == null || name.isBlank()) throw new IllegalArgumentException("name must not be blank");
|
||||||
|
|
||||||
|
String sql = "INSERT INTO tln (name, info, owner_id, is_public, allow_subdomains) VALUES (?, ?, ?, ?, ?)";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
ps.setString(1, name.trim());
|
||||||
|
ps.setString(2, info);
|
||||||
|
if (ownerId == null) ps.setNull(3, Types.INTEGER);
|
||||||
|
else ps.setInt(3, ownerId);
|
||||||
|
ps.setInt(4, isPublic ? 1 : 0);
|
||||||
|
ps.setInt(5, allowSubdomains ? 1 : 0);
|
||||||
|
|
||||||
|
ps.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("No generated key returned for tln.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds TLN by name.
|
||||||
|
*
|
||||||
|
* @param name tln.name
|
||||||
|
* @return optional row
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public Optional<TlnRow> findTlnByName(String name) throws SQLException {
|
||||||
|
if (name == null || name.isBlank()) return Optional.empty();
|
||||||
|
|
||||||
|
String sql = "SELECT id, name, info, owner_id, is_public, allow_subdomains FROM tln WHERE name = ? LIMIT 1";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setString(1, name.trim());
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return Optional.empty();
|
||||||
|
return Optional.of(mapTln(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Permissions ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists TLNs visible to a user: owned OR public OR unowned+public.
|
||||||
|
*
|
||||||
|
* @param userId current user id
|
||||||
|
* @return rows
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public TlnRow[] listVisibleTlns(int userId) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT id, name, info, owner_id, is_public, allow_subdomains
|
||||||
|
FROM tln
|
||||||
|
WHERE is_public = 1 OR owner_id = ?
|
||||||
|
ORDER BY name ASC
|
||||||
|
""";
|
||||||
|
|
||||||
|
List<TlnRow> out = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setInt(1, userId);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapTln(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toArray(new TlnRow[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates TLN fields except name (owner-only).
|
||||||
|
*
|
||||||
|
* @param tlnId tln.id
|
||||||
|
* @param ownerUserId users.id (must match tln.owner_id)
|
||||||
|
* @param newInfo new info text (nullable)
|
||||||
|
* @param isPublic is_public
|
||||||
|
* @param allowSubdomains allow_subdomains
|
||||||
|
* @return true if updated, false if not owned/not found
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public boolean updateTlnOwned(int tlnId, int ownerUserId, String newInfo, boolean isPublic, boolean allowSubdomains) throws SQLException {
|
||||||
|
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
|
||||||
|
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||||
|
|
||||||
|
String sql = "UPDATE tln SET info = ?, is_public = ?, allow_subdomains = ? WHERE id = ? AND owner_id = ?";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setString(1, newInfo);
|
||||||
|
ps.setInt(2, isPublic ? 1 : 0);
|
||||||
|
ps.setInt(3, allowSubdomains ? 1 : 0);
|
||||||
|
ps.setInt(4, tlnId);
|
||||||
|
ps.setInt(5, ownerUserId);
|
||||||
|
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- InfoNames ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes TLN (owner-only).
|
||||||
|
*
|
||||||
|
* @param tlnId tln.id
|
||||||
|
* @param ownerUserId users.id
|
||||||
|
* @return true if deleted
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public boolean deleteTlnOwned(int tlnId, int ownerUserId) throws SQLException {
|
||||||
|
if (tlnId <= 0) throw new IllegalArgumentException("tlnId must be > 0");
|
||||||
|
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||||
|
|
||||||
|
String sql = "DELETE FROM tln WHERE id = ? AND owner_id = ?";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setInt(1, tlnId);
|
||||||
|
ps.setInt(2, ownerUserId);
|
||||||
|
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an InfoName under the given TLN.
|
||||||
|
*
|
||||||
|
* @param tln TLN row
|
||||||
|
* @param info infonames.info
|
||||||
|
* @param userId infonames.uid (users.id)
|
||||||
|
* @return new infonames.id
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public int createInfoName(TlnRow tln, String info, int userId) throws SQLException {
|
||||||
|
if (tln == null) throw new IllegalArgumentException("tln must not be null");
|
||||||
|
if (userId <= 0) throw new IllegalArgumentException("userId must be > 0");
|
||||||
|
if (!canUseTln(tln, userId)) throw new SQLException("TLN not public and not owned by user.");
|
||||||
|
if (info == null || info.isBlank()) throw new IllegalArgumentException("info must not be blank");
|
||||||
|
|
||||||
|
String sql = "INSERT INTO infonames (info, tln_id, uid) VALUES (?, ?, ?)";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
ps.setString(1, info.trim());
|
||||||
|
ps.setInt(2, tln.id);
|
||||||
|
ps.setInt(3, userId);
|
||||||
|
|
||||||
|
ps.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("No generated key returned for infonames.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a user owns the infoname.
|
||||||
|
*
|
||||||
|
* @param infonameId infonames.id
|
||||||
|
* @param userId users.id
|
||||||
|
* @return true if owned
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public boolean isOwnerOfInfoName(int infonameId, int userId) throws SQLException {
|
||||||
|
if (infonameId <= 0 || userId <= 0) return false;
|
||||||
|
|
||||||
|
String sql = "SELECT 1 FROM infonames WHERE id = ? AND uid = ? LIMIT 1";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setInt(1, infonameId);
|
||||||
|
ps.setInt(2, userId);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an InfoName (owner-only).
|
||||||
|
*
|
||||||
|
* @param infonameId infonames.id
|
||||||
|
* @param ownerUserId users.id
|
||||||
|
* @return true if deleted
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public boolean deleteInfoNameOwned(int infonameId, int ownerUserId) throws SQLException {
|
||||||
|
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||||
|
if (ownerUserId <= 0) throw new IllegalArgumentException("ownerUserId must be > 0");
|
||||||
|
|
||||||
|
String sql = "DELETE FROM infonames WHERE id = ? AND uid = ?";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setInt(1, infonameId);
|
||||||
|
ps.setInt(2, ownerUserId);
|
||||||
|
|
||||||
|
return ps.executeUpdate() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Subnames + Records ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists InfoNames owned by user (joined with TLN data).
|
||||||
|
*
|
||||||
|
* @param userId users.id
|
||||||
|
* @return rows
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public InfoNameRow[] listOwnedInfoNames(int userId) throws SQLException {
|
||||||
|
if (userId <= 0) return new InfoNameRow[0];
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
SELECT i.id AS iid, i.info AS info, i.tln_id AS tln_id,
|
||||||
|
t.name AS tln_name, t.info AS tln_info, t.owner_id AS owner_id,
|
||||||
|
t.is_public AS is_public, t.allow_subdomains AS allow_subdomains
|
||||||
|
FROM infonames i
|
||||||
|
INNER JOIN tln t ON t.id = i.tln_id
|
||||||
|
WHERE i.uid = ?
|
||||||
|
ORDER BY t.name ASC, i.info ASC
|
||||||
|
""";
|
||||||
|
|
||||||
|
List<InfoNameRow> out = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setInt(1, userId);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
TlnRow tln = new TlnRow(
|
||||||
|
rs.getInt("tln_id"),
|
||||||
|
rs.getString("tln_name"),
|
||||||
|
rs.getString("tln_info"),
|
||||||
|
(Integer) rs.getObject("owner_id"),
|
||||||
|
rs.getInt("is_public") == 1,
|
||||||
|
rs.getInt("allow_subdomains") == 1
|
||||||
|
);
|
||||||
|
out.add(new InfoNameRow(
|
||||||
|
rs.getInt("iid"),
|
||||||
|
rs.getString("info"),
|
||||||
|
tln
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toArray(new InfoNameRow[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a subname exists for an infoname; returns subnames.id.
|
||||||
|
*
|
||||||
|
* <p>If sub is null/blank, returns null (root).</p>
|
||||||
|
*
|
||||||
|
* @param infonameId infonames.id
|
||||||
|
* @param sub sub label (nullable)
|
||||||
|
* @return subnames.id or null for root
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public Integer ensureSubname(int infonameId, String sub) throws SQLException {
|
||||||
|
if (sub == null || sub.isBlank()) return null;
|
||||||
|
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||||
|
|
||||||
|
String name = sub.trim();
|
||||||
|
|
||||||
|
// 1) find
|
||||||
|
String find = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ? LIMIT 1";
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(find)) {
|
||||||
|
|
||||||
|
ps.setInt(1, infonameId);
|
||||||
|
ps.setString(2, name);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getInt("id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) insert
|
||||||
|
String ins = "INSERT INTO subnames (name, infoname_id) VALUES (?, ?)";
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(ins, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
ps.setString(1, name);
|
||||||
|
ps.setInt(2, infonameId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("No generated key returned for subnames.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a DNS-like record.
|
||||||
|
*
|
||||||
|
* @param infonameId infonames.id
|
||||||
|
* @param subnameId subnames.id or null for root
|
||||||
|
* @param type enum type (A/AAAA/TXT/CNAME/MX/SRV/NS)
|
||||||
|
* @param value record value
|
||||||
|
* @param ttl ttl (default 3600)
|
||||||
|
* @param priority priority (MX/SRV)
|
||||||
|
* @param port port (SRV)
|
||||||
|
* @param weight weight (SRV)
|
||||||
|
* @return new records.id
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public int addRecord(int infonameId,
|
||||||
|
Integer subnameId,
|
||||||
|
String type,
|
||||||
|
String value,
|
||||||
|
int ttl,
|
||||||
|
Integer priority,
|
||||||
|
Integer port,
|
||||||
|
Integer weight) throws SQLException {
|
||||||
|
|
||||||
|
if (infonameId <= 0) throw new IllegalArgumentException("infonameId must be > 0");
|
||||||
|
if (type == null || type.isBlank()) throw new IllegalArgumentException("type must not be blank");
|
||||||
|
if (value == null || value.isBlank()) throw new IllegalArgumentException("value must not be blank");
|
||||||
|
if (ttl <= 0) ttl = 3600;
|
||||||
|
|
||||||
|
String t = type.trim().toUpperCase();
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO records (infoname_id, subname_id, type, value, ttl, priority, port, weight)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
ps.setInt(1, infonameId);
|
||||||
|
|
||||||
|
if (subnameId == null) ps.setNull(2, Types.INTEGER);
|
||||||
|
else ps.setInt(2, subnameId);
|
||||||
|
|
||||||
|
ps.setString(3, t);
|
||||||
|
ps.setString(4, value.trim());
|
||||||
|
ps.setInt(5, ttl);
|
||||||
|
|
||||||
|
if (priority == null) ps.setNull(6, Types.INTEGER);
|
||||||
|
else ps.setInt(6, priority);
|
||||||
|
|
||||||
|
if (port == null) ps.setNull(7, Types.INTEGER);
|
||||||
|
else ps.setInt(7, port);
|
||||||
|
|
||||||
|
if (weight == null) ps.setNull(8, Types.INTEGER);
|
||||||
|
else ps.setInt(8, weight);
|
||||||
|
|
||||||
|
ps.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("No generated key returned for records.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLN row.
|
||||||
|
*/
|
||||||
|
public record TlnRow(int id, String name, String info, Integer ownerId, boolean isPublic, boolean allowSubdomains) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InfoName row (includes TLN).
|
||||||
|
*/
|
||||||
|
public record InfoNameRow(int id, String info, TlnRow tln) {
|
||||||
|
}
|
||||||
|
}
|
||||||
122
frontend/utils/RegistrationService.java
Normal file
122
frontend/utils/RegistrationService.java
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package ins.frontend.utils;
|
||||||
|
|
||||||
|
import org.openautonomousconnection.webserver.utils.PasswordHasher;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration service for creating users with secure password hashing and basic validation.
|
||||||
|
*/
|
||||||
|
public final class RegistrationService {
|
||||||
|
|
||||||
|
private static final int USERNAME_MIN = 5;
|
||||||
|
private static final int USERNAME_MAX = 256;
|
||||||
|
private static final int PASSWORD_MIN = 6;
|
||||||
|
private static final int PASSWORD_MAX = 256;
|
||||||
|
|
||||||
|
private final UserDao userDao;
|
||||||
|
private final PasswordHasher hasher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a registration service.
|
||||||
|
*
|
||||||
|
* @param userDao user DAO
|
||||||
|
* @param hasher password hasher
|
||||||
|
*/
|
||||||
|
public RegistrationService(UserDao userDao, PasswordHasher hasher) {
|
||||||
|
this.userDao = Objects.requireNonNull(userDao, "userDao");
|
||||||
|
this.hasher = Objects.requireNonNull(hasher, "hasher");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeUsername(String u) {
|
||||||
|
if (u == null) return null;
|
||||||
|
String t = u.trim();
|
||||||
|
return t.isEmpty() ? null : t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String validate(String username, String password) {
|
||||||
|
if (username == null) return "Missing username.";
|
||||||
|
if (password == null) return "Missing password.";
|
||||||
|
|
||||||
|
if (username.length() < USERNAME_MIN) return "Username too short (min " + USERNAME_MIN + ").";
|
||||||
|
if (username.length() > USERNAME_MAX) return "Username too long (max " + USERNAME_MAX + ").";
|
||||||
|
|
||||||
|
// Allow only a safe subset to avoid weird edge cases in UI and future.
|
||||||
|
for (int i = 0; i < username.length(); i++) {
|
||||||
|
char c = username.charAt(i);
|
||||||
|
boolean ok = (c >= 'a' && c <= 'z')
|
||||||
|
|| (c >= 'A' && c <= 'Z')
|
||||||
|
|| (c >= '0' && c <= '9')
|
||||||
|
|| c == '_' || c == '-' || c == '.';
|
||||||
|
if (!ok) return "Username contains invalid characters.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length() < PASSWORD_MIN) return "Password too short (min " + PASSWORD_MIN + ").";
|
||||||
|
if (password.length() > PASSWORD_MAX) return "Password too long.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user.
|
||||||
|
*
|
||||||
|
* @param usernameRaw raw username (from form)
|
||||||
|
* @param passwordRaw raw password (from form)
|
||||||
|
* @return result containing either userId or an error message
|
||||||
|
*/
|
||||||
|
public Result register(String usernameRaw, String passwordRaw) {
|
||||||
|
String username = normalizeUsername(usernameRaw);
|
||||||
|
String password = (passwordRaw == null) ? "" : passwordRaw;
|
||||||
|
|
||||||
|
String validationError = validate(username, password);
|
||||||
|
if (validationError != null) {
|
||||||
|
return Result.error(validationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Choose: store username as-is OR store sha256(username).
|
||||||
|
// Your schema says: username(sha256 hex or plain) -> keep it plain for now.
|
||||||
|
String usernameStored = username;
|
||||||
|
|
||||||
|
if (userDao.findByUsername(usernameStored).isPresent()) {
|
||||||
|
return Result.error("Username already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String passwordEncoded = hasher.hash(password);
|
||||||
|
int userId = userDao.createUserWithNewUuid(usernameStored, passwordEncoded);
|
||||||
|
|
||||||
|
return Result.ok(userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error("Registration failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration result.
|
||||||
|
*
|
||||||
|
* @param ok whether succeeded
|
||||||
|
* @param userId created user id, or -1
|
||||||
|
* @param error error message, or null
|
||||||
|
*/
|
||||||
|
public record Result(boolean ok, int userId, String error) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a success result.
|
||||||
|
*
|
||||||
|
* @param userId user id
|
||||||
|
* @return result
|
||||||
|
*/
|
||||||
|
public static Result ok(int userId) {
|
||||||
|
return new Result(true, userId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an error result.
|
||||||
|
*
|
||||||
|
* @param error error message
|
||||||
|
* @return result
|
||||||
|
*/
|
||||||
|
public static Result error(String error) {
|
||||||
|
return new Result(false, -1, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
frontend/utils/UserDao.java
Normal file
154
frontend/utils/UserDao.java
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package ins.frontend.utils;
|
||||||
|
|
||||||
|
import org.openautonomousconnection.webserver.utils.Sha256;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for users table:
|
||||||
|
* users(id, uid(uuid string), username(sha256 hex or plain), password(pbkdf2...))
|
||||||
|
*/
|
||||||
|
public final class UserDao {
|
||||||
|
|
||||||
|
private final DataSourceProvider dataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a UserDao.
|
||||||
|
*
|
||||||
|
* @param dataSource connection provider
|
||||||
|
*/
|
||||||
|
public UserDao(DataSourceProvider dataSource) {
|
||||||
|
this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a user by username.
|
||||||
|
*
|
||||||
|
* @param uid exact uid match
|
||||||
|
* @return optional row
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public Optional<UserRow> findByUid(UUID uid) throws SQLException {
|
||||||
|
if (uid == null) return Optional.empty();
|
||||||
|
|
||||||
|
String sql = "SELECT id, uid, username, password FROM users WHERE uid = ? LIMIT 1";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setString(1, uid.toString());
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return Optional.empty();
|
||||||
|
return Optional.of(new UserRow(
|
||||||
|
rs.getInt("id"),
|
||||||
|
rs.getString("uid"),
|
||||||
|
rs.getString("username"),
|
||||||
|
rs.getString("password")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user.
|
||||||
|
*
|
||||||
|
* @param uid uuid string (36 chars)
|
||||||
|
* @param username username string (your choice: raw or sha256 hex)
|
||||||
|
* @param passwordEncoded encoded password
|
||||||
|
* @return generated users.id
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public int createUser(String uid, String username, String passwordEncoded) throws SQLException {
|
||||||
|
if (uid == null || uid.isBlank()) throw new IllegalArgumentException("uid must not be blank");
|
||||||
|
if (username == null || username.isBlank()) throw new IllegalArgumentException("username must not be blank");
|
||||||
|
if (passwordEncoded == null || passwordEncoded.isBlank())
|
||||||
|
throw new IllegalArgumentException("passwordEncoded must not be blank");
|
||||||
|
|
||||||
|
String sql = "INSERT INTO users (uid, username, password) VALUES (?, ?, ?)";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
|
||||||
|
ps.setString(1, uid);
|
||||||
|
ps.setString(2, Sha256.hex(username));
|
||||||
|
ps.setString(3, passwordEncoded);
|
||||||
|
ps.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.getGeneratedKeys()) {
|
||||||
|
if (rs.next()) return rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SQLException("No generated key returned for users.id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: creates a user with a new random UUID.
|
||||||
|
*
|
||||||
|
* @param username username
|
||||||
|
* @param passwordEncoded encoded password
|
||||||
|
* @return generated users.id
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public int createUserWithNewUuid(String username, String passwordEncoded) throws SQLException {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
while (findByUid(uuid).isPresent()) uuid = UUID.randomUUID();
|
||||||
|
return createUser(uuid.toString(), username, passwordEncoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a user by username.
|
||||||
|
*
|
||||||
|
* @param username exact username match
|
||||||
|
* @return optional row
|
||||||
|
* @throws SQLException on SQL errors
|
||||||
|
*/
|
||||||
|
public Optional<UserRow> findByUsername(String username) throws SQLException {
|
||||||
|
if (username == null || username.isBlank()) return Optional.empty();
|
||||||
|
username = Sha256.hex(username);
|
||||||
|
|
||||||
|
String sql = "SELECT id, uid, username, password FROM users WHERE username = ? LIMIT 1";
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setString(1, username);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return Optional.empty();
|
||||||
|
return Optional.of(new UserRow(
|
||||||
|
rs.getInt("id"),
|
||||||
|
rs.getString("uid"),
|
||||||
|
rs.getString("username"),
|
||||||
|
rs.getString("password")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection provider abstraction.
|
||||||
|
*/
|
||||||
|
public interface DataSourceProvider {
|
||||||
|
/**
|
||||||
|
* @return open SQL connection
|
||||||
|
* @throws SQLException on errors
|
||||||
|
*/
|
||||||
|
Connection getConnection() throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User row.
|
||||||
|
*
|
||||||
|
* @param id users.id
|
||||||
|
* @param uid users.uid (uuid)
|
||||||
|
* @param username users.username
|
||||||
|
* @param passwordEncoded users.password
|
||||||
|
*/
|
||||||
|
public record UserRow(int id, String uid, String username, String passwordEncoded) {
|
||||||
|
}
|
||||||
|
}
|
||||||
113
frontend/utils/WebApp.java
Normal file
113
frontend/utils/WebApp.java
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package ins.frontend.utils;
|
||||||
|
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||||
|
import org.openautonomousconnection.webserver.utils.PasswordHasher;
|
||||||
|
import org.openautonomousconnection.webserver.utils.Pbkdf2Sha256Hasher;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application singleton holding DAO + password hasher.
|
||||||
|
*/
|
||||||
|
public final class WebApp {
|
||||||
|
|
||||||
|
private static volatile WebApp INSTANCE;
|
||||||
|
private static boolean isInit;
|
||||||
|
private static UserDao.DataSourceProvider dsp = null;
|
||||||
|
private final UserDao userDao;
|
||||||
|
private final RegistrarDao registrarDao;
|
||||||
|
private final PasswordHasher passwordHasher;
|
||||||
|
|
||||||
|
private WebApp(UserDao userDao, RegistrarDao registrarDao, PasswordHasher passwordHasher) {
|
||||||
|
this.userDao = Objects.requireNonNull(userDao, "userDao");
|
||||||
|
this.registrarDao = Objects.requireNonNull(registrarDao, "registrarDao");
|
||||||
|
this.passwordHasher = Objects.requireNonNull(passwordHasher, "passwordHasher");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the singleton (call once at server startup).
|
||||||
|
*/
|
||||||
|
public static void init() {
|
||||||
|
if (isInit) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!new File(new File(".").getParent(), "db.properties").exists()) {
|
||||||
|
new File(new File(".").getParent(), "db.properties").createNewFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationManager config = new ConfigurationManager(new File(new File(".").getParent(), "db.properties"));
|
||||||
|
config.loadProperties();
|
||||||
|
|
||||||
|
if (!config.isSet("db.url")) {
|
||||||
|
config.set(
|
||||||
|
"db.url",
|
||||||
|
"jdbc:mariadb://localhost:3306/ins?useUnicode=true&characterEncoding=utf8"
|
||||||
|
);
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("db.user")) {
|
||||||
|
config.set("db.user", "username");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("db.password")) {
|
||||||
|
config.set("db.password", "password");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp = () -> {
|
||||||
|
try {
|
||||||
|
isInit = true;
|
||||||
|
|
||||||
|
return DriverManager.getConnection(config.getString("db.url"), config.getString("db.user"), config.getString("db.password"));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new SQLException("Failed to open DB connection", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to create config", e);
|
||||||
|
}
|
||||||
|
UserDao udao = new UserDao(dsp);
|
||||||
|
RegistrarDao rdao = new RegistrarDao(dsp);
|
||||||
|
PasswordHasher hasher = new Pbkdf2Sha256Hasher(150_000, 16, 32);
|
||||||
|
|
||||||
|
INSTANCE = new WebApp(udao, rdao, hasher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the initialized instance.
|
||||||
|
*
|
||||||
|
* @return app
|
||||||
|
*/
|
||||||
|
public static WebApp get() {
|
||||||
|
WebApp v = INSTANCE;
|
||||||
|
if (v == null) throw new IllegalStateException("Oac2WebApp not initialized. Call Oac2WebApp.init(...) first.");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return user DAO
|
||||||
|
*/
|
||||||
|
public UserDao users() {
|
||||||
|
return userDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return registrar DAO
|
||||||
|
*/
|
||||||
|
public RegistrarDao dao() {
|
||||||
|
return registrarDao;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return password hasher
|
||||||
|
*/
|
||||||
|
public PasswordHasher passwordHasher() {
|
||||||
|
return passwordHasher;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
pom.xml
103
pom.xml
@@ -1,22 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>DNSServer</artifactId>
|
<artifactId>INSServer</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0.1-BETA.0.3</version>
|
||||||
<organization>
|
<organization>
|
||||||
<name>Open Autonomous Connection</name>
|
<name>Open Autonomous Connection</name>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
</organization>
|
</organization>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
<description>The default DNS-Server</description>
|
<description>The default INS-Server</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>23</maven.compiler.source>
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
<maven.compiler.target>23</maven.compiler.target>
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -45,14 +45,13 @@
|
|||||||
|
|
||||||
<issueManagement>
|
<issueManagement>
|
||||||
<system>Issue Tracker</system>
|
<system>Issue Tracker</system>
|
||||||
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/DNSServer/issues</url>
|
<url>https://repo.open-autonomous-connection.org/open-autonomous-connection/INSServer/issues</url>
|
||||||
</issueManagement>
|
</issueManagement>
|
||||||
|
|
||||||
<licenses>
|
<licenses>
|
||||||
<license>
|
<license>
|
||||||
<name>Open Autonomous Public License</name>
|
<name>Open Autonomous Public License (OAPL)</name>
|
||||||
<url>https://repo.open-autonomous-connection.org/Open-Autonomous-Connection/OAPL/</url>
|
<url>https://open-autonomous-connection.org/license.html</url>
|
||||||
<distribution>repo</distribution>
|
|
||||||
</license>
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
|
|
||||||
@@ -69,8 +68,86 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>protocol</artifactId>
|
<artifactId>Protocol</artifactId>
|
||||||
<version>1.0.0-BETA.7</version>
|
<version>1.0.1-BETA.0.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
|
<version>3.5.7</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.openautonomousconnection.insserver.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>--add-exports</arg>
|
||||||
|
<arg>java.base/sun.security.x509=ALL-UNNAMED</arg>
|
||||||
|
<arg>--add-exports</arg>
|
||||||
|
<arg>java.base/sun.security.util=ALL-UNNAMED</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>3.6.3</version>
|
||||||
|
<configuration>
|
||||||
|
<failOnError>false</failOnError>
|
||||||
|
<failOnWarnings>false</failOnWarnings>
|
||||||
|
<doclint>none</doclint>
|
||||||
|
<locale>en_US</locale>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<docencoding>UTF-8</docencoding>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-javadocs</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
c5RF/V4VtgM7jggSLlUUgO0L2/Hw5bM=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCGKkc5obUKeeX8
|
|
||||||
gDZff4YUoKIoWHMkdxNxpt2UjR7I0TcFVXbOdpEFG1jBLrlcPvTs1LaJ7exQ9657
|
|
||||||
wma9OMzhn6Lmtp6ecahKGMpnMHSUjFUe45nRtNur6g5i5T5rYcnzaxJBaSxzcV/r
|
|
||||||
hI/KXRIbxEh+w7/X39wN3ze6EKdQWgHkv7Wsw9R5aGQBtWOQLnj+5wnveJDl3te+
|
|
||||||
B4oQ60fokW6Dt4RRe4u/YRyu+2VvG8x3RF/OQpDz4qFZHO6hGcS+hbNRyiIkhvdB
|
|
||||||
a8Qqu1bbDyVc80bkiiPDTO/FQVCxalDMTF7N00ng80OelLs4T+cwR/KQO9NWwgzM
|
|
||||||
6gJ38aplAgMBAAECggEAEQE5ztWunxnNKyBLA/ajVWj4tJllWqlXBxApxILQdI6D
|
|
||||||
5UKsUMXZYqbRLIcSCl2WaJAbZMcJrUd/T4NXx0L0QOKdYJWj7BwmwuF6gfKzzAIv
|
|
||||||
Zb9eRySftR3w3dsFetHFqXsYML82WZl0mWLPYMxEF83cjGieH6vpdb6ljwk+U6It
|
|
||||||
4WkyxUPojBsTdhmc+T1wI8NpegeRvNy/7euXZXaoo//Rf/fBUFRfSvVfDSMcKDIq
|
|
||||||
H4dcMAFc2ZHVE2wUuwqLbNbI8L/yuBgP5CHxnHSqG5nZQNMX3hg5winnAyp/iwnR
|
|
||||||
VA072TeuxqLx68qHEMhhZqO6Te63VtgBpYPapGT4UQKBgQC6Hwq9vXeL7JWsiZqX
|
|
||||||
Suh8kj/puy/CSeuWBopJVBJkGjyP3DDKtoJqEeOeMajL8rSW3RaCUdv4FvilGfc4
|
|
||||||
9zTuJBSZTMpoUaCpuX1k6jYOtwcMe17KdJB7Sr1bjKef3VdOpSA+pG3lElz1sH7t
|
|
||||||
sG6hdIx3mhJB/HpYKGN4A4h8GQKBgQC4iX/E1mvdDVnXcE4cFN/exSYQFmtNDBYe
|
|
||||||
o6QbLsjEqISAOxipLRTZhQjvePzSD4Y4F7jKqNcvK6LZnQiE43C6VImMT8UT7C2a
|
|
||||||
ht96l3/bDEGEls1HrzqVVepCWQGnKltkOU69HDRYXJCGJwZ5hfx66UtsXR0ICSNH
|
|
||||||
EHBoZE3qLQKBgAx4Xfjeg+79F/4qbhAq9a67ActAPm/vEfjIUWWeW2kXlO0ynJZI
|
|
||||||
ai5/KlgEDcI0bcMZ5xMuNuXFbD1rovPPJF28TyECUyFwLqqQggVL5/lObAt7DJvV
|
|
||||||
+YQ5OryyjNyaMOpVB/cKf050z4OqoqZ8Rr1MmMi+qvB+4RedBSUaX3+pAoGACwbM
|
|
||||||
R6q4T0EY+TKQuETXC6ykFZoBV109hR8qEyW8gWPAZxkg5Br5f/XfDtAf6z1aO8fP
|
|
||||||
Fuz3zq3A0Vf8xlQAzGF4xpWNpR8bqnwcpmqfDSuyToXkRkBGM94qXUSMQLzbMSXr
|
|
||||||
eolQ52bAjAOQ83n6GC4Qf60gqvZA0WI+FT7JGRECgYAi54HhK7HFogAlX58EX77h
|
|
||||||
YHL5KHBlG+l8v4amIKvD0JclYylwyLhYkmIM+T4mh8I+T1nabcNeNWit+CYoLwQ8
|
|
||||||
A3P0ij67TkEV3Ud7KKOtYty+getkM9/hwm14LlMzfLZKay9HCsbb5E2TgmShF68F
|
|
||||||
VtECuunYyfN4wJSE0aWRPg==
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
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
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
5FB2F5A2CA8357D37C84CC406B5F81617348EB58
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEWjCCAkKgAwIBAgIUX7L1osqDV9N8hMxAa1+BYXNI61gwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
|
||||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA5MjExMTU3MzFaFw0yNzEy
|
|
||||||
MjUxMTU3MzFaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
|
||||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
|
||||||
AQUAA4IBDwAwggEKAoIBAQCGKkc5obUKeeX8gDZff4YUoKIoWHMkdxNxpt2UjR7I
|
|
||||||
0TcFVXbOdpEFG1jBLrlcPvTs1LaJ7exQ9657wma9OMzhn6Lmtp6ecahKGMpnMHSU
|
|
||||||
jFUe45nRtNur6g5i5T5rYcnzaxJBaSxzcV/rhI/KXRIbxEh+w7/X39wN3ze6EKdQ
|
|
||||||
WgHkv7Wsw9R5aGQBtWOQLnj+5wnveJDl3te+B4oQ60fokW6Dt4RRe4u/YRyu+2Vv
|
|
||||||
G8x3RF/OQpDz4qFZHO6hGcS+hbNRyiIkhvdBa8Qqu1bbDyVc80bkiiPDTO/FQVCx
|
|
||||||
alDMTF7N00ng80OelLs4T+cwR/KQO9NWwgzM6gJ38aplAgMBAAGjQjBAMB0GA1Ud
|
|
||||||
DgQWBBTW7Nng9W84V+q0TknoTempPZo2KjAfBgNVHSMEGDAWgBQRmpjWjaugPtg0
|
|
||||||
ohgTtX+h1LPW7DANBgkqhkiG9w0BAQsFAAOCAgEAguDRKxW8QFV7fJlzAgzjwsBu
|
|
||||||
97Bk5IsmcIU7sOlmOFYZAG0hwqfuvzU/Tv48tuXOvGQsJtdNSfe4qybd5HnkxmTs
|
|
||||||
ZmqzCsIh9P4PL/KrKTCykEP935468wg/X9QkykBOJx2zOgBEllxo33BNG4ie6R3H
|
|
||||||
tYy/1y/hsGfi4Ma2jqBaPfblI86t9VoKiPqqKqFgGk32F4NqIJ2hMKxSypznHIC7
|
|
||||||
IPikKx9Gt8EpwT+ytJKOnd/A7nBEiaDw5ubcR1s6pmlcGLAE1+A9TX2ncRUcvMI4
|
|
||||||
w2M5i7X/Vz2zQiPEWmQliYDG1ZFeD1dth973muM6H6NvFyXvEzzRZl5K5DwAcgqA
|
|
||||||
uMvEKDiDPwKROBTzjq3WIHBe49fz96Fpxld5JxH3RhC2PzIYjUquU2P8Ah6SN6SZ
|
|
||||||
+Mf/61QxNInnEcxNjknGhcfiyhoqu2B+1/N5cfpXyBfgTQfVzV3HiUvAHzcza9yp
|
|
||||||
mnPrrxRodkXwncMJNJYTr36yTyZPxC0TZUycvOPwEfBQ1vESAeiJ7+QgFGFu0lgQ
|
|
||||||
1XAJVpCfHpDrkT184/pszyHHzydWvP7GcrPgJVNlXbMvpenhm2QTCaft5TUDj9+m
|
|
||||||
8quLjm+bj0OQq1Fmv/OlWvycC5fxnYA0JlRDz56QPHkEL7aZ/+apZhB6Eb77/etv
|
|
||||||
A5go7PJCZ9vnblPW/HE=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#Sat Oct 04 01:25:22 CEST 2025
|
|
||||||
server.site.info=DNS-SERVER INFO SITE IP
|
|
||||||
server.site.register=SERVER IP TO DNS-FRONTENT WEBSITE
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package org.openautonomousconnection.dns;
|
|
||||||
|
|
||||||
import org.openautonomousconnection.protocol.side.dns.ConnectedProtocolClient;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.handlers.ClassicHandlerDNSServer;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_Domain;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.objects.Classic_RequestDomain;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.classic.utils.Classic_ProtocolVersion;
|
|
||||||
|
|
||||||
import java.sql.SQLException;
|
|
||||||
|
|
||||||
public class ClassicHandler extends ClassicHandlerDNSServer {
|
|
||||||
@Override
|
|
||||||
public void handleMessage(ConnectedProtocolClient client, String message, Classic_ProtocolVersion protocolVersion) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Classic_Domain getDomain(Classic_RequestDomain requestDomain) throws SQLException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Classic_Domain ping(Classic_RequestDomain requestDomain) throws SQLException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unsupportedClassicPacket(String className, Object[] content, ConnectedProtocolClient client) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package org.openautonomousconnection.dns;
|
|
||||||
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandManager;
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission;
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
|
|
||||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
|
||||||
import org.openautonomousconnection.protocol.ProtocolSettings;
|
|
||||||
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.Scanner;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
private static final CommandPermission PERMISSION_ALL = new CommandPermission("all", 1);
|
|
||||||
private static final CommandExecutor commandExecutor = new CommandExecutor("DNS", PERMISSION_ALL) {};
|
|
||||||
private static CommandManager commandManager;
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
ProtocolSettings settings = new ProtocolSettings();
|
|
||||||
new ProtocolBridge(new Server(), settings, ProtocolVersion.PV_1_0_0_BETA, new File("logs"));
|
|
||||||
ProtocolBridge.getInstance().setClassicHandlerDNSServer(new ClassicHandler());
|
|
||||||
commandManager = new CommandManager(ProtocolBridge.getInstance().getProtocolSettings().eventManager);
|
|
||||||
Scanner scanner = new Scanner(System.in);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
System.out.println(commandExecutor.getName() + "> ");
|
|
||||||
String line = scanner.nextLine();
|
|
||||||
commandManager.execute(commandExecutor, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package org.openautonomousconnection.dns;
|
|
||||||
|
|
||||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
|
||||||
import org.openautonomousconnection.protocol.side.dns.ConnectedProtocolClient;
|
|
||||||
import org.openautonomousconnection.protocol.side.dns.ProtocolDNSServer;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.DNSResponseCode;
|
|
||||||
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.Domain;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Server extends ProtocolDNSServer {
|
|
||||||
/**
|
|
||||||
* Constructs a ProtocolDNSServer with the specified configuration file.
|
|
||||||
*
|
|
||||||
* @throws IOException If an I/O error occurs.
|
|
||||||
* @throws CertificateException If a certificate error occurs.
|
|
||||||
*/
|
|
||||||
public Server() throws IOException, CertificateException {
|
|
||||||
super(new File("config.properties"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Domain> getDomains() {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDomainDestination(Domain domain) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSubnameDestination(Domain domain, String subname) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTLNInfoSite(String topLevelName) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DNSResponseCode validateDomain(Domain requestedDomain) {
|
|
||||||
if (!requestedDomain.getProtocol().equalsIgnoreCase("oac")) return DNSResponseCode.RESPONSE_INVALID_REQUEST;
|
|
||||||
return DNSResponseCode.RESPONSE_DOMAIN_FULLY_NOT_EXIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validationPacketSendFailed(Domain domain, ConnectedProtocolClient client, Exception exception) {
|
|
||||||
ProtocolBridge.getInstance().getLogger().exception("Failed to send ValidationPacket. (" +
|
|
||||||
"Domain: " + domain.getProtocol() + "." + (domain.hasSubname() ? domain.getSubname() : "") + "." + domain.getTopLevelName() + "/" + domain.getPath() + "?" + domain.getQuery() + "#" + domain.getFragment() + ";" +
|
|
||||||
";Client: " + client.getConnectionHandler().getClientID() + ")", exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void domainDestinationPacketFailedSend(ConnectedProtocolClient client, Domain domain, DNSResponseCode validationResponse, Exception exception) {
|
|
||||||
ProtocolBridge.getInstance().getLogger().exception("Failed to send DomainDestinationPacket. (" +
|
|
||||||
"Domain: " + domain.getProtocol() + "." + (domain.hasSubname() ? domain.getSubname() : "") + "." + domain.getTopLevelName() + "/" + domain.getPath() + "?" + domain.getQuery() + "#" + domain.getFragment() + ";" +
|
|
||||||
"Validation response: " + validationResponse + ";Client: " + client.getConnectionHandler().getClientID() + ")", exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
package org.openautonomousconnection.insserver;
|
||||||
|
|
||||||
|
import org.openautonomousconnection.protocol.side.ins.ProtocolINSServer;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecord;
|
||||||
|
import org.openautonomousconnection.protocol.versions.v1_0_0.beta.INSRecordType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database-backed INS server.
|
||||||
|
* <p>
|
||||||
|
* This implementation resolves records from the SQL schema and performs CNAME recursion (with loop detection + depth limit).
|
||||||
|
* Returned records are deterministically sorted so that callers can safely select index 0 as the "best" record.
|
||||||
|
*/
|
||||||
|
public final class DatabaseINSServer extends ProtocolINSServer {
|
||||||
|
|
||||||
|
private final String jdbcUrl;
|
||||||
|
private final String jdbcUser;
|
||||||
|
private final String jdbcPassword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new database-backed INS server.
|
||||||
|
*
|
||||||
|
* @throws IOException If the base server initialization fails.
|
||||||
|
* @throws CertificateException If required certificate files are missing or invalid.
|
||||||
|
*/
|
||||||
|
public DatabaseINSServer(String insInfoSite, String insFrontendSite, String jdbcUrl, String jdbcUser, String jdbcPassword) throws Exception {
|
||||||
|
super(insInfoSite, insFrontendSite);
|
||||||
|
|
||||||
|
this.jdbcUrl = jdbcUrl;
|
||||||
|
this.jdbcUser = jdbcUser;
|
||||||
|
this.jdbcPassword = jdbcPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int safeInt(int v) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safeString(String s) {
|
||||||
|
return s == null ? "" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Connection openConnection() throws SQLException {
|
||||||
|
return DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a request for an INS record based on TLN, name, subname and record type.
|
||||||
|
* <p>
|
||||||
|
* The implementation:
|
||||||
|
* <ul>
|
||||||
|
* <li>Locates the corresponding InfoName in the SQL schema</li>
|
||||||
|
* <li>Returns all matching {@link INSRecord} entries</li>
|
||||||
|
* <li>Performs CNAME recursion (with loop detection + depth limit) when no direct records for the requested type exist</li>
|
||||||
|
* <li>Returns deterministically sorted results (so index 0 can be used as "best record")</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param tln The top-level name.
|
||||||
|
* @param name The InfoName.
|
||||||
|
* @param sub Optional subname, may be {@code null}.
|
||||||
|
* @param type The requested record type.
|
||||||
|
* @return Resolved records (possibly empty).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<INSRecord> resolve(String tln, String name, String sub, INSRecordType type) {
|
||||||
|
try {
|
||||||
|
List<INSRecord> out = resolveInternal(tln, name, sub, type, 0, new HashSet<>());
|
||||||
|
out.sort(recordComparator(type));
|
||||||
|
return out;
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
getProtocolBridge().getProtocolValues().logger.exception(
|
||||||
|
"INS resolve failed for " + formatName(tln, name, sub) + " type=" + type,
|
||||||
|
ex
|
||||||
|
);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the TLN info site which is used when a client queries {@code info.<tln>} without any sub-name.
|
||||||
|
* <p>
|
||||||
|
* The value is read from {@code tln.info} and must be of the form {@code "host:port"}.
|
||||||
|
*
|
||||||
|
* @param tln The TLN name.
|
||||||
|
* @return The configured info target ("host:port") or {@code null} if not present.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String resolveTLNInfoSite(String tln) {
|
||||||
|
String sql = "SELECT info FROM tln WHERE name = ?";
|
||||||
|
|
||||||
|
try (Connection conn = openConnection();
|
||||||
|
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
|
||||||
|
ps.setString(1, tln);
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getString("info");
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
getProtocolBridge().getProtocolValues().logger.exception("Failed to resolve TLN info site for tln=" + tln, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatName(String tln, String name, String sub) {
|
||||||
|
if (sub == null || sub.isEmpty()) return name + "." + tln;
|
||||||
|
return sub + "." + name + "." + tln;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal recursive resolver with CNAME handling.
|
||||||
|
*
|
||||||
|
* @param depth Current recursion depth.
|
||||||
|
* @param visited Loop detection set of canonical names (sub.name.tln).
|
||||||
|
*/
|
||||||
|
private List<INSRecord> resolveInternal(
|
||||||
|
String tln,
|
||||||
|
String name,
|
||||||
|
String sub,
|
||||||
|
INSRecordType requestedType,
|
||||||
|
int depth,
|
||||||
|
Set<String> visited
|
||||||
|
) throws SQLException {
|
||||||
|
|
||||||
|
final int MAX_CNAME_DEPTH = 16;
|
||||||
|
|
||||||
|
String canonical = formatName(tln, name, sub).toLowerCase(Locale.ROOT);
|
||||||
|
if (!visited.add(canonical)) {
|
||||||
|
// loop detected
|
||||||
|
getProtocolBridge().getProtocolValues().logger.warn("CNAME loop detected for " + canonical + " type=" + requestedType);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth > MAX_CNAME_DEPTH) {
|
||||||
|
getProtocolBridge().getProtocolValues().logger.warn("CNAME recursion limit exceeded for " + canonical + " type=" + requestedType);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection conn = openConnection()) {
|
||||||
|
Integer tlnId = findTLNId(conn, tln);
|
||||||
|
if (tlnId == null) return new ArrayList<>();
|
||||||
|
|
||||||
|
Integer infoNameId = findInfoNameId(conn, tlnId, name);
|
||||||
|
if (infoNameId == null) return new ArrayList<>();
|
||||||
|
|
||||||
|
Integer subNameId = findSubNameId(conn, infoNameId, sub);
|
||||||
|
|
||||||
|
// 1) direct records
|
||||||
|
List<INSRecord> direct = loadRecords(conn, infoNameId, subNameId, requestedType);
|
||||||
|
direct.sort(recordComparator(requestedType));
|
||||||
|
|
||||||
|
// If the requested type is CNAME, do not recurse.
|
||||||
|
if (requestedType == INSRecordType.CNAME) return direct;
|
||||||
|
|
||||||
|
if (!direct.isEmpty()) return direct;
|
||||||
|
|
||||||
|
// 2) fallback to CNAME if no direct records exist
|
||||||
|
List<INSRecord> cnames = loadRecords(conn, infoNameId, subNameId, INSRecordType.CNAME);
|
||||||
|
cnames.sort(recordComparator(INSRecordType.CNAME));
|
||||||
|
|
||||||
|
if (cnames.isEmpty()) return new ArrayList<>();
|
||||||
|
|
||||||
|
List<INSRecord> aggregated = new ArrayList<>();
|
||||||
|
|
||||||
|
for (INSRecord cname : cnames) {
|
||||||
|
TargetName target = parseCnameTarget(cname.value);
|
||||||
|
if (target == null) {
|
||||||
|
getProtocolBridge().getProtocolValues().logger.warn("Invalid CNAME target '" + cname.value + "' on " + canonical);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recurse on the target name to fetch the original requested type
|
||||||
|
List<INSRecord> resolvedTarget = resolveInternal(
|
||||||
|
target.tln,
|
||||||
|
target.name,
|
||||||
|
target.sub,
|
||||||
|
requestedType,
|
||||||
|
depth + 1,
|
||||||
|
visited
|
||||||
|
);
|
||||||
|
aggregated.addAll(resolvedTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregated.sort(recordComparator(requestedType));
|
||||||
|
return aggregated;
|
||||||
|
} finally {
|
||||||
|
// important: visited is shared across the recursion chain on purpose
|
||||||
|
// (do not remove canonical here; loop detection should stay for the whole chain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic ordering for returned records. The goal is stable ranking so callers can pick index 0.
|
||||||
|
*
|
||||||
|
* <p>Rules:
|
||||||
|
* <ul>
|
||||||
|
* <li>priority ASC (smaller is better)</li>
|
||||||
|
* <li>weight DESC (larger is better)</li>
|
||||||
|
* <li>port ASC (smaller is better)</li>
|
||||||
|
* <li>ttl DESC (larger is better)</li>
|
||||||
|
* <li>value ASC (case-insensitive) as final tie-breaker</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param requestedType The type being requested (kept for future type-specific tuning).
|
||||||
|
* @return Comparator for {@link INSRecord}.
|
||||||
|
*/
|
||||||
|
private Comparator<INSRecord> recordComparator(INSRecordType requestedType) {
|
||||||
|
return Comparator
|
||||||
|
.comparingInt((INSRecord r) -> safeInt(r.priority))
|
||||||
|
.thenComparingInt(r -> -safeInt(r.weight))
|
||||||
|
.thenComparingInt(r -> safeInt(r.port))
|
||||||
|
.thenComparingInt(r -> -safeInt(r.ttl))
|
||||||
|
.thenComparing(r -> safeString(r.value), String.CASE_INSENSITIVE_ORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all records of a given type for (infoname_id, subname_id).
|
||||||
|
*
|
||||||
|
* @param type May be {@code null} to load all types.
|
||||||
|
*/
|
||||||
|
private List<INSRecord> loadRecords(Connection conn, int infonameId, Integer subnameId, INSRecordType type) throws SQLException {
|
||||||
|
StringBuilder sql = new StringBuilder(
|
||||||
|
"SELECT type, value, ttl, priority, port, weight " +
|
||||||
|
"FROM records " +
|
||||||
|
"WHERE infoname_id = ? "
|
||||||
|
);
|
||||||
|
|
||||||
|
if (subnameId == null) sql.append("AND subname_id IS NULL ");
|
||||||
|
else sql.append("AND subname_id = ? ");
|
||||||
|
|
||||||
|
if (type != null) sql.append("AND type = ? ");
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(sql.toString())) {
|
||||||
|
int idx = 1;
|
||||||
|
ps.setInt(idx++, infonameId);
|
||||||
|
|
||||||
|
if (subnameId != null) ps.setInt(idx++, subnameId);
|
||||||
|
|
||||||
|
if (type != null) ps.setString(idx, type.name());
|
||||||
|
|
||||||
|
List<INSRecord> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
INSRecordType rType = INSRecordType.valueOf(rs.getString("type"));
|
||||||
|
String value = rs.getString("value");
|
||||||
|
int ttl = rs.getInt("ttl");
|
||||||
|
int priority = rs.getInt("priority");
|
||||||
|
int port = rs.getInt("port");
|
||||||
|
int weight = rs.getInt("weight");
|
||||||
|
|
||||||
|
result.add(new INSRecord(rType, value, priority, weight, port, ttl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer findTLNId(Connection conn, String tln) throws SQLException {
|
||||||
|
String sql = "SELECT id FROM tln WHERE name = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, tln);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getInt("id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer findInfoNameId(Connection conn, int tlnId, String infoName) throws SQLException {
|
||||||
|
String sql = "SELECT id FROM infonames WHERE tln_id = ? AND info = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, tlnId);
|
||||||
|
ps.setString(2, infoName);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getInt("id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer findSubNameId(Connection conn, int infoNameId, String sub) throws SQLException {
|
||||||
|
if (sub == null || sub.isEmpty()) return null;
|
||||||
|
|
||||||
|
String sql = "SELECT id FROM subnames WHERE infoname_id = ? AND name = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, infoNameId);
|
||||||
|
ps.setString(2, sub);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) return rs.getInt("id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a CNAME target string into TLN, InfoName and optional subname.
|
||||||
|
*
|
||||||
|
* @param value Raw CNAME value (e.g. "sub.app.example" or "example.net").
|
||||||
|
* @return Parsed {@link TargetName} or {@code null} if invalid.
|
||||||
|
*/
|
||||||
|
private TargetName parseCnameTarget(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String trimmed = value.trim();
|
||||||
|
if (trimmed.isEmpty()) return null;
|
||||||
|
|
||||||
|
String[] parts = trimmed.split("\\.");
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
|
||||||
|
String tln = parts[parts.length - 1];
|
||||||
|
String name = parts[parts.length - 2];
|
||||||
|
String sub = null;
|
||||||
|
|
||||||
|
if (parts.length > 2) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < parts.length - 2; i++) {
|
||||||
|
if (i > 0) sb.append('.');
|
||||||
|
sb.append(parts[i]);
|
||||||
|
}
|
||||||
|
sub = sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TargetName(tln, name, sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TargetName {
|
||||||
|
final String tln;
|
||||||
|
final String name;
|
||||||
|
final String sub;
|
||||||
|
|
||||||
|
TargetName(String tln, String name, String sub) {
|
||||||
|
this.tln = tln;
|
||||||
|
this.name = name;
|
||||||
|
this.sub = sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package org.openautonomousconnection.dns;
|
package org.openautonomousconnection.insserver;
|
||||||
|
|
||||||
import dev.unlegitdqrk.unlegitlibrary.command.events.CommandExecutorMissingPermissionEvent;
|
import dev.unlegitdqrk.unlegitlibrary.command.events.CommandExecutorMissingPermissionEvent;
|
||||||
import dev.unlegitdqrk.unlegitlibrary.command.events.CommandNotFoundEvent;
|
import dev.unlegitdqrk.unlegitlibrary.command.events.CommandNotFoundEvent;
|
||||||
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
import dev.unlegitdqrk.unlegitlibrary.event.EventListener;
|
||||||
import org.openautonomousconnection.protocol.ProtocolBridge;
|
|
||||||
|
|
||||||
public class Listener extends EventListener {
|
public class Listener extends EventListener {
|
||||||
|
|
||||||
@@ -11,11 +10,11 @@ public class Listener extends EventListener {
|
|||||||
public void onCommandNotFound(CommandNotFoundEvent event) {
|
public void onCommandNotFound(CommandNotFoundEvent event) {
|
||||||
StringBuilder argsBuilder = new StringBuilder();
|
StringBuilder argsBuilder = new StringBuilder();
|
||||||
for (String arg : event.getArgs()) argsBuilder.append(arg).append(" ");
|
for (String arg : event.getArgs()) argsBuilder.append(arg).append(" ");
|
||||||
ProtocolBridge.getInstance().getLogger().error("Command '" + event.getName() + argsBuilder.toString() + "' not found!");
|
Main.getProtocolBridge().getProtocolValues().logger.error("Command '" + event.getName() + argsBuilder.toString() + "' not found!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@dev.unlegitdqrk.unlegitlibrary.event.Listener
|
@dev.unlegitdqrk.unlegitlibrary.event.Listener
|
||||||
public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) {
|
public void onMissingCommandPermission(CommandExecutorMissingPermissionEvent event) {
|
||||||
ProtocolBridge.getInstance().getLogger().error("You do not have enough permissions to execute this command!");
|
Main.getProtocolBridge().getProtocolValues().logger.error("You do not have enough permissions to execute this command!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
109
src/main/java/org/openautonomousconnection/insserver/Main.java
Normal file
109
src/main/java/org/openautonomousconnection/insserver/Main.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package org.openautonomousconnection.insserver;
|
||||||
|
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.addon.AddonLoader;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.command.CommandManager;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.command.CommandPermission;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.file.ConfigurationManager;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.openautonomousconnection.insserver.commands.StopCommand;
|
||||||
|
import org.openautonomousconnection.protocol.ProtocolBridge;
|
||||||
|
import org.openautonomousconnection.protocol.ProtocolValues;
|
||||||
|
import org.openautonomousconnection.protocol.versions.ProtocolVersion;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
public static final CommandPermission PERMISSION_ALL = new CommandPermission("all", 1);
|
||||||
|
private static final CommandExecutor commandExecutor = new CommandExecutor("INS", PERMISSION_ALL) {
|
||||||
|
};
|
||||||
|
@Getter
|
||||||
|
private static CommandManager commandManager;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static ProtocolBridge protocolBridge;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private static ProtocolValues values;
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
if (!new File("logs").exists()) new File("logs").mkdir();
|
||||||
|
Logger logger = new Logger(new File("logs"), false, true);
|
||||||
|
|
||||||
|
values = new ProtocolValues();
|
||||||
|
values.packetHandler = new PacketHandler();
|
||||||
|
values.eventManager = new EventManager();
|
||||||
|
values.protocolVersion = ProtocolVersion.PV_1_0_1_BETA;
|
||||||
|
values.eventManager.registerListener(new Listener());
|
||||||
|
values.logger = logger;
|
||||||
|
|
||||||
|
if (!new File("config.properties").exists()) {
|
||||||
|
new File("config.properties").createNewFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationManager config = new ConfigurationManager(new File("config.properties"));
|
||||||
|
config.loadProperties();
|
||||||
|
|
||||||
|
if (!config.isSet("db.url")) {
|
||||||
|
config.set(
|
||||||
|
"db.url",
|
||||||
|
"jdbc:mariadb://localhost:3306/ins?useUnicode=true&characterEncoding=utf8"
|
||||||
|
);
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("db.user")) {
|
||||||
|
config.set("db.user", "username");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("db.password")) {
|
||||||
|
config.set("db.password", "password");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("port")) {
|
||||||
|
config.set("port", 1026);
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("ins.info")) {
|
||||||
|
config.set("ins.info", "INS INFO SITE (HOST)");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.isSet("ins.frontend")) {
|
||||||
|
config.set("ins.frontend", "INS FRONTEND SITE (HOST)");
|
||||||
|
config.saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = config.getString("db.url");
|
||||||
|
String user = config.getString("db.user");
|
||||||
|
String password = config.getString("db.password");
|
||||||
|
|
||||||
|
int tcpPort = config.getInt("port");
|
||||||
|
|
||||||
|
String info = config.getString("ins.info");
|
||||||
|
String frontend = config.getString("ins.frontend");
|
||||||
|
|
||||||
|
DatabaseINSServer server = new DatabaseINSServer(info, frontend, url, user, password);
|
||||||
|
protocolBridge = new ProtocolBridge(server, values);
|
||||||
|
server.start(tcpPort);
|
||||||
|
|
||||||
|
commandManager = new CommandManager(values.eventManager);
|
||||||
|
commandManager.registerCommand(StopCommand.class);
|
||||||
|
|
||||||
|
Scanner scanner = new Scanner(System.in);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
System.out.print(commandExecutor.getName() + "> ");
|
||||||
|
String line = scanner.nextLine();
|
||||||
|
commandManager.execute(commandExecutor, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.openautonomousconnection.insserver.commands;
|
||||||
|
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.command.Command;
|
||||||
|
import dev.unlegitdqrk.unlegitlibrary.command.CommandExecutor;
|
||||||
|
import org.openautonomousconnection.insserver.Main;
|
||||||
|
|
||||||
|
import javax.management.InstanceAlreadyExistsException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class StopCommand extends Command {
|
||||||
|
|
||||||
|
public StopCommand() throws InstanceAlreadyExistsException {
|
||||||
|
super(Main.getCommandManager(), "stop", "Stop the Server", "stop", Collections.singletonList(Main.PERMISSION_ALL), new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandExecutor commandExecutor, String s, String[] strings) {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user