From 82edcf6a5f061bd7a4374435f12c0337fd84edbf Mon Sep 17 00:00:00 2001 From: mohammed adib Date: Tue, 21 Jul 2026 12:32:58 +0530 Subject: [PATCH] create the resumable index store with owner-only permissions --- .../PropertiesBasedResumableProcessor.java | 48 ++++++++---- ...rtiesBasedResumableProcessorStoreTest.java | 75 +++++++++++++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java index 3a65f019e7..2a98131851 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java @@ -16,15 +16,27 @@ import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileNotFoundException; import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Map; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.WRITE; import static org.asynchttpclient.util.MiscUtils.closeSilently; /** @@ -35,9 +47,19 @@ public class PropertiesBasedResumableProcessor implements ResumableAsyncHandler. private static final Logger log = LoggerFactory.getLogger(PropertiesBasedResumableProcessor.class); private static final File TMP = new File(System.getProperty("java.io.tmpdir"), "ahc"); private static final String storeName = "ResumableAsyncHandler.properties"; + private static final boolean POSIX = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + private static final FileAttribute[] DIR_ATTRIBUTES = ownerOnlyAttributes("rwx------"); + private static final FileAttribute[] FILE_ATTRIBUTES = ownerOnlyAttributes("rw-------"); + private static final Set CREATE_OPTIONS = new HashSet<>(Arrays.asList(WRITE, CREATE_NEW)); private final ConcurrentHashMap properties = new ConcurrentHashMap<>(); + private static FileAttribute[] ownerOnlyAttributes(String permissions) { + return POSIX + ? new FileAttribute[]{PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(permissions))} + : new FileAttribute[0]; + } + private static String append(Map.Entry e) { return e.getKey() + '=' + e.getValue() + '\n'; } @@ -60,18 +82,18 @@ public void save(Map map) { OutputStream os = null; try { - if (!TMP.exists() && !TMP.mkdirs()) { - throw new IllegalStateException("Unable to create directory: " + TMP.getAbsolutePath()); - } - File f = new File(TMP, storeName); - if (!f.exists() && !f.createNewFile()) { - throw new IllegalStateException("Unable to create temp file: " + f.getAbsolutePath()); - } - if (!f.canWrite()) { - throw new IllegalStateException(); + Path dir = TMP.toPath(); + if (!Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectory(dir, DIR_ATTRIBUTES); } - os = Files.newOutputStream(f.toPath()); + // The store sits at a fixed path in the shared temp directory and holds the URLs being + // downloaded, so it is recreated here with owner-only permissions instead of being written + // through whatever is already at that path. CREATE_NEW after the delete fails rather than + // opens if another local user re-plants a file or a symlink in between. + Path f = dir.resolve(storeName); + Files.deleteIfExists(f); + os = Channels.newOutputStream(Files.newByteChannel(f, CREATE_OPTIONS, FILE_ATTRIBUTES)); for (Map.Entry e : properties.entrySet()) { os.write(append(e).getBytes(UTF_8)); } @@ -87,7 +109,7 @@ public void save(Map map) { public Map load() { Scanner scan = null; try { - scan = new Scanner(new File(TMP, storeName), UTF_8); + scan = new Scanner(Files.newInputStream(new File(TMP, storeName).toPath(), LinkOption.NOFOLLOW_LINKS), UTF_8); scan.useDelimiter("[=\n]"); String key; @@ -98,7 +120,7 @@ public Map load() { properties.put(key, Long.valueOf(value)); } log.debug("Loading previous download state {}", properties); - } catch (FileNotFoundException ex) { + } catch (NoSuchFileException ex) { log.debug("Missing {}", storeName); } catch (Throwable ex) { // Survive any exceptions diff --git a/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java new file mode 100644 index 0000000000..5662fb51b4 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.handler.resumable; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Covers how the resumable index store is created in the shared temp directory. + */ +public class PropertiesBasedResumableProcessorStoreTest { + + private static final Path STORE = Paths.get(System.getProperty("java.io.tmpdir"), "ahc", "ResumableAsyncHandler.properties"); + + private static void deleteStore() throws IOException { + Files.deleteIfExists(STORE); + } + + @Test + public void storeIsCreatedOwnerOnly() throws IOException { + assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); + deleteStore(); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/owner-only.url", 15L); + processor.save(null); + + assertEquals("rw-------", PosixFilePermissions.toString(Files.getPosixFilePermissions(STORE))); + } + + @Test + public void saveDoesNotWriteThroughASymlink() throws IOException { + assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix")); + deleteStore(); + Files.createDirectories(STORE.getParent()); + + Path target = Files.createTempFile("ahc-symlink-target", ".txt"); + try { + Files.write(target, "untouched".getBytes(StandardCharsets.UTF_8)); + Files.createSymbolicLink(STORE, target); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/symlink.url", 15L); + processor.save(null); + + assertEquals("untouched", new String(Files.readAllBytes(target), StandardCharsets.UTF_8)); + } finally { + Files.deleteIfExists(target); + deleteStore(); + } + } +}