Skip to content

Commit 8d3d0ad

Browse files
authored
Merge pull request #73 from BenCodez/codex/http-transport-library
Add secure HTTP server communication transport
2 parents fdec696 + 95ee1b3 commit 8d3d0ad

32 files changed

Lines changed: 9288 additions & 5 deletions

‎SimpleAPI/pom.xml‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<maven.compiler.source>21</maven.compiler.source>
1717
<maven.compiler.target>21</maven.compiler.target>
1818
<maven.compiler.release>21</maven.compiler.release>
19+
<bouncycastle.version>1.85</bouncycastle.version>
1920
</properties>
2021
<build>
2122
<sourceDirectory>src/main/java</sourceDirectory>
@@ -71,6 +72,11 @@
7172
<finalName>${project.name}</finalName>
7273
</configuration>
7374
</plugin>
75+
<plugin>
76+
<groupId>org.apache.maven.plugins</groupId>
77+
<artifactId>maven-surefire-plugin</artifactId>
78+
<version>3.5.4</version>
79+
</plugin>
7480
<plugin>
7581
<groupId>org.apache.maven.plugins</groupId>
7682
<artifactId>maven-shade-plugin</artifactId>
@@ -155,6 +161,16 @@
155161
</repository>
156162
</repositories>
157163
<dependencies>
164+
<dependency>
165+
<groupId>org.bouncycastle</groupId>
166+
<artifactId>bcpkix-jdk18on</artifactId>
167+
<version>${bouncycastle.version}</version>
168+
</dependency>
169+
<dependency>
170+
<groupId>org.bouncycastle</groupId>
171+
<artifactId>bcprov-jdk18on</artifactId>
172+
<version>${bouncycastle.version}</version>
173+
</dependency>
158174
<dependency>
159175
<groupId>org.spigotmc</groupId>
160176
<artifactId>spigot-api</artifactId>
@@ -407,4 +423,4 @@
407423
</build>
408424
</profile>
409425
</profiles>
410-
</project>
426+
</project>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.bencodez.simpleapi.file;
2+
3+
import java.io.IOException;
4+
import java.nio.channels.FileChannel;
5+
import java.nio.file.AccessDeniedException;
6+
import java.nio.file.Files;
7+
import java.nio.file.LinkOption;
8+
import java.nio.file.Path;
9+
import java.nio.file.StandardOpenOption;
10+
import java.util.Locale;
11+
12+
/** Cross-platform helpers for forcing file contents and published directory entries. */
13+
public final class DurableFiles {
14+
private DurableFiles() { }
15+
16+
public static void forceFile(Path file) throws IOException {
17+
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
18+
channel.force(true);
19+
}
20+
}
21+
22+
public static void forceDirectory(Path directory) throws IOException {
23+
if (directory == null) return;
24+
try {
25+
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
26+
channel.force(true);
27+
}
28+
} catch (AccessDeniedException unsupportedDirectoryHandle) {
29+
// The Windows NIO provider cannot open directory handles. File contents are
30+
// still forced before atomic publication; do not make persistence unusable.
31+
if (!isWindowsName(System.getProperty("os.name", ""))) throw unsupportedDirectoryHandle;
32+
} catch (UnsupportedOperationException unsupportedDirectoryForce) {
33+
// Some providers support atomic moves but expose no directory-force operation.
34+
}
35+
}
36+
37+
public static boolean deleteIfExists(Path target) throws IOException {
38+
boolean deleted = Files.deleteIfExists(target);
39+
if (deleted) forceDirectory(target.toAbsolutePath().normalize().getParent());
40+
return deleted;
41+
}
42+
43+
public static void forceMoveDirectories(Path source, Path target) throws IOException {
44+
try {
45+
Path sourceParent = source.toAbsolutePath().normalize().getParent();
46+
Path targetParent = target.toAbsolutePath().normalize().getParent();
47+
forceDirectory(targetParent);
48+
if (sourceParent != null && !sourceParent.equals(targetParent)) forceDirectory(sourceParent);
49+
} catch (IOException failure) {
50+
throw new PublishedException(failure);
51+
}
52+
}
53+
54+
public static boolean isWindowsName(String name) {
55+
return name != null && name.trim().toLowerCase(Locale.ROOT).startsWith("windows");
56+
}
57+
58+
/** Indicates that an atomic rename completed before metadata writeback failed. */
59+
@SuppressWarnings("serial")
60+
public static final class PublishedException extends IOException {
61+
public PublishedException(IOException cause) {
62+
super("File was published but its directory metadata could not be forced", cause);
63+
}
64+
}
65+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.bencodez.simpleapi.file;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.LinkOption;
6+
import java.nio.file.Path;
7+
import java.nio.file.attribute.AclEntry;
8+
import java.nio.file.attribute.AclEntryPermission;
9+
import java.nio.file.attribute.AclEntryType;
10+
import java.nio.file.attribute.AclFileAttributeView;
11+
import java.nio.file.attribute.GroupPrincipal;
12+
import java.nio.file.attribute.PosixFilePermission;
13+
import java.nio.file.attribute.UserPrincipal;
14+
import java.util.EnumSet;
15+
import java.util.List;
16+
import java.util.Set;
17+
18+
/** Enforces and verifies owner-only access for persisted private material. */
19+
public final class PrivateFilePermissions {
20+
private static final Set<PosixFilePermission> OWNER_FILE = EnumSet.of(
21+
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
22+
private static final Set<PosixFilePermission> OWNER_DIRECTORY = EnumSet.of(
23+
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
24+
private static final Set<AclEntryPermission> OWNER_ACL = EnumSet.allOf(AclEntryPermission.class);
25+
26+
private PrivateFilePermissions() { }
27+
28+
/** Enforces 0600-equivalent access, or fails when the provider cannot prove it. */
29+
public static void ownerOnlyFile(Path path) throws IOException {
30+
enforce(path, OWNER_FILE);
31+
}
32+
33+
/** Enforces 0700-equivalent access, or fails when the provider cannot prove it. */
34+
public static void ownerOnlyDirectory(Path path) throws IOException {
35+
enforce(path, OWNER_DIRECTORY);
36+
}
37+
38+
private static void enforce(Path path, Set<PosixFilePermission> permissions) throws IOException {
39+
if (path == null) throw new IllegalArgumentException("Private path is required");
40+
if (Files.isSymbolicLink(path)) throw new IOException("Refusing symbolic link for private storage: " + path);
41+
try {
42+
Files.setPosixFilePermissions(path, permissions);
43+
if (!Files.getPosixFilePermissions(path, LinkOption.NOFOLLOW_LINKS).equals(permissions))
44+
throw new IOException("Could not verify owner-only POSIX permissions for " + path);
45+
return;
46+
} catch (UnsupportedOperationException unsupported) {
47+
// A non-POSIX provider must expose an ACL that can be reduced and verified.
48+
}
49+
enforceWithAcl(path);
50+
}
51+
52+
private static void enforceWithAcl(Path path) throws IOException {
53+
try {
54+
AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
55+
if (view == null) throw new IOException("Owner-only private storage is unsupported for " + path);
56+
enforceWithAcl(view, path);
57+
} catch (UnsupportedOperationException unsupported) {
58+
throw new IOException("Owner-only private storage is unsupported for " + path, unsupported);
59+
}
60+
}
61+
62+
/** Package-visible for deterministic ACL-provider tests. */
63+
static void enforceWithAcl(AclFileAttributeView view, Path path) throws IOException {
64+
try {
65+
if (view == null) throw new IOException("Owner-only private storage is unsupported for " + path);
66+
UserPrincipal owner = view.getOwner();
67+
if (owner == null || owner instanceof GroupPrincipal)
68+
throw new IOException("Private storage owner is not an individual account for " + path);
69+
AclEntry ownerEntry = AclEntry.newBuilder().setType(AclEntryType.ALLOW).setPrincipal(owner)
70+
.setPermissions(OWNER_ACL).build();
71+
view.setAcl(List.of(ownerEntry));
72+
List<AclEntry> acl = view.getAcl();
73+
if (acl.size() != 1 || !isVerifiedOwnerEntry(acl.get(0), owner))
74+
throw new IOException("Could not verify owner-only ACL permissions for " + path);
75+
} catch (UnsupportedOperationException unsupported) {
76+
throw new IOException("Owner-only private storage is unsupported for " + path, unsupported);
77+
}
78+
}
79+
80+
private static boolean isVerifiedOwnerEntry(AclEntry entry, UserPrincipal owner) {
81+
return entry.type() == AclEntryType.ALLOW && owner.equals(entry.principal()) && entry.flags().isEmpty()
82+
&& entry.permissions().equals(OWNER_ACL);
83+
}
84+
}

0 commit comments

Comments
 (0)