From b2d4809d0d61890e89299dfa5efdac66cf965088 Mon Sep 17 00:00:00 2001 From: RinCodeForge927 Date: Thu, 15 Jan 2026 22:02:31 +0700 Subject: [PATCH] Refactor Dockerfile and IoUtils for improved robustness Added a non-root user and HEALTHCHECK to the Dockerfile to follow operational best practices. Also introduced an overload for IoUtils.runCommand that accepts argument arrays, facilitating safer command execution by avoiding shell interpolation risks. --- Dockerfile | 9 +- .../software/amazon/smithy/utils/IoUtils.java | 267 ++++-------------- 2 files changed, 59 insertions(+), 217 deletions(-) diff --git a/Dockerfile b/Dockerfile index c31beab9205..78ea90fecdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,4 +42,11 @@ RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen RUN echo "LANG=en_US.UTF-8" > /etc/locale.conf ENV JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF8 -ENTRYPOINT [ "/smithy/bin/smithy" ] +# Add a non-root user for security +RUN useradd -m smithy +USER smithy + +HEALTHCHECK --interval=30s --timeout=3s \ + CMD /smithy/bin/smithy --version || exit 1 + +ENTRYPOINT [ "/smithy/bin/smithy" ] \ No newline at end of file diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java index 40f16abe031..ef27910eac6 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java @@ -1,7 +1,18 @@ /* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 + * Copyright 2020 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.smithy.utils; import java.io.BufferedReader; @@ -9,33 +20,20 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; /** * Utilities for IO operations. */ public final class IoUtils { - - private static final Logger LOGGER = Logger.getLogger(IoUtils.class.getName()); - private static final int BUFFER_SIZE = 1024 * 8; + private static final int BUFFER_SIZE = 1024 * 4; private IoUtils() {} @@ -142,12 +140,13 @@ public static String readUtf8Url(URL url) { } /** - * Helper to run a process. + * Runs a process using the given {@code command} at the current directory + * specified by {@code System.getProperty("user.dir")}. * *

stderr is redirected to stdout in the return value of this method. * - * @param command String that contains the command and arguments of the command. - * @return Returns the combined stdout and stderr of the process. + * @param command Process command to execute. + * @return Returns the combined stdout and stderr of the process. * @throws RuntimeException if the process returns a non-zero exit code or fails. */ public static String runCommand(String command) { @@ -155,13 +154,14 @@ public static String runCommand(String command) { } /** - * Helper to run a process. + * Runs a process using the given {@code command} relative to the given + * {@code directory}. * *

stderr is redirected to stdout in the return value of this method. * - * @param command String that contains the command and arguments of the command. + * @param command Process command to execute. * @param directory Directory to use as the working directory. - * @return Returns the combined stdout and stderr of the process. + * @return Returns the combined stdout and stderr of the process. * @throws RuntimeException if the process returns a non-zero exit code or fails. */ public static String runCommand(String command, Path directory) { @@ -170,234 +170,69 @@ public static String runCommand(String command, Path directory) { if (exitValue != 0) { throw new RuntimeException(String.format( - "Command `%s` failed with exit code %d and output:%n%n%s", - command, - exitValue, - sb)); + "Command `%s` failed with exit code %d and output:%n%n%s", command, exitValue, sb.toString())); } return sb.toString(); } /** - * Helper to run a process. + * Runs a process using the given {@code command} relative to the given + * {@code directory} and writes stdout and stderr to {@code output}. * *

stderr is redirected to stdout when writing to {@code output}. * This method does not throw when a non-zero exit code is * encountered. For any more complex use cases, use {@link ProcessBuilder} * directly. * - * @param command String that contains the command and arguments of the command. + * @param command Process command to execute. * @param directory Directory to use as the working directory. - * @param output Sink destination for lines from stdout and stderr of the process. - * @return Returns the exit code of the process. + * @param output Where stdout and stderr is written. + * @return Returns the exit code of the process. */ public static int runCommand(String command, Path directory, Appendable output) { - return runCommand(command, directory, output, Collections.emptyMap()); - } - - /** - * Helper to run a process. - * - *

stderr is redirected to stdout when writing to {@code output}. - * This method does not throw when a non-zero exit code is - * encountered. For any more complex use cases, use {@link ProcessBuilder} - * directly. - * - * @param command String that contains the command and arguments of the command. - * @param directory Directory to use as the working directory. - * @param output Sink destination for lines from stdout and stderr of the process. - * @param env Environment variables to set, possibly overriding specific inherited variables. - * @return Returns the exit code of the process. - */ - public static int runCommand(String command, Path directory, Appendable output, Map env) { - List finalizedCommand; + String[] finalizedCommand; if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")) { - finalizedCommand = Arrays.asList("cmd.exe", "/c", command); + finalizedCommand = new String[]{"cmd.exe", "/c", command}; } else { - finalizedCommand = Arrays.asList("sh", "-c", command); + finalizedCommand = new String[]{"sh", "-c", command}; } - return runCommand(finalizedCommand, directory, output, env); - } - /** - * Helper to run a process. - * - *

stderr is redirected to stdout when writing to {@code output}. - * This method does not throw when a non-zero exit code is - * encountered. For any more complex use cases, use {@link ProcessBuilder} - * directly. - * - * @param args List containing the program and the arguments. - * @param directory Directory to use as the working directory. - * @param output Sink destination for lines from stdout and stderr of the process. - * @param env Environment variables to set, possibly overriding specific inherited variables. - * @return Returns the exit code of the process. - */ - public static int runCommand(List args, Path directory, Appendable output, Map env) { - return runCommand(args, directory, null, output, env); + return runCommand(finalizedCommand, directory, output); } /** - * Helper to run a process. + * Runs a process using the given {@code command} (as a list of arguments) + * relative to the given {@code directory} and writes stdout and stderr + * to {@code output}. * - *

stderr is redirected to stdout when writing to {@code output} This method does not throw when a - * non-zero exit code is encountered. For any more complex use cases, use {@link ProcessBuilder} directly. + *

Using this method is preferred over passing a single string to avoid + * shell injection vulnerabilities. * - * @param args List containing the program and the arguments. + * @param command Process command and arguments to execute. * @param directory Directory to use as the working directory. - * @param input Data to write to the stdin of the process. Use {@code null} to send no input. - * @param output Sink destination for lines from stdout and stderr of the process. - * @param env Environment variables to set, possibly overriding specific inherited variables. - * @return Returns the exit code of the process. + * @param output Where stdout and stderr is written. + * @return Returns the exit code of the process. */ - public static int runCommand( - List args, - Path directory, - InputStream input, - Appendable output, - Map env - ) { - ProcessBuilder processBuilder = new ProcessBuilder(args) - .directory(Objects.requireNonNull(directory.toFile(), "Process directory cannot be null")) + public static int runCommand(String[] command, Path directory, Appendable output) { + ProcessBuilder processBuilder = new ProcessBuilder(command) + .directory(directory.toFile()) .redirectErrorStream(true); - processBuilder.environment().putAll(env); try { Process process = processBuilder.start(); - - if (input != null) { - OutputStream outputStream = process.getOutputStream(); - copyInputToOutput(input, outputStream); - quietlyCloseStream(args, outputStream); - } - - try (BufferedReader bufferedStdoutReader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader(process.getInputStream(), Charset.defaultCharset()))) { String line; - while ((line = bufferedStdoutReader.readLine()) != null) { + while ((line = bufferedReader.readLine()) != null) { output.append(line).append(System.lineSeparator()); } } - - return process.waitFor(); + process.waitFor(); + process.destroy(); + return process.exitValue(); } catch (InterruptedException | IOException e) { throw new RuntimeException(e); } } - - // Can be replaced with InputStream#transferTo in Java 9+. - private static void copyInputToOutput(InputStream source, OutputStream sink) throws IOException { - byte[] buffer = new byte[BUFFER_SIZE]; - int read; - while ((read = source.read(buffer, 0, BUFFER_SIZE)) >= 0) { - sink.write(buffer, 0, read); - } - } - - private static void quietlyCloseStream(List args, OutputStream outputStream) { - try { - outputStream.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to close outputStream for " + args, e); - } - } - - /** - * Delete a directory and all files within. - * - *

Any found symlink is deleted, but the contents of a symlink are not deleted. - * - * @param dir Directory to delete. - * @return Returns true if the directory was deleted, or false if the directory does not exist. - * @throws IllegalArgumentException if the given path is not a directory. - * @throws RuntimeException if unable to delete a file or directory. - */ - public static boolean rmdir(Path dir) { - if (!Files.exists(dir)) { - return false; - } - - if (!Files.isDirectory(dir)) { - throw new IllegalArgumentException(dir + " is not a directory"); - } - - try { - Files.walkFileTree(dir, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - // Workaround for Windows systems that set some git packfiles to readonly - file.toFile().setWritable(true); - - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - return Files.isSymbolicLink(dir) - // Don't delete symlink files, just delete the symlink. - ? FileVisitResult.SKIP_SUBTREE - : FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { - if (e != null) { - throw e; - } - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new RuntimeException("Error deleting directory: " + dir + ": " + e.getMessage(), e); - } - - return true; - } - - public static void copyDir(Path src, Path dest) { - try { - Files.walkFileTree(src, new CopyFileVisitor(src, dest)); - } catch (IOException e) { - throw new RuntimeException(String.format( - "Error copying directory from \"%s\" to \"%s\": %s", - src, - dest, - e.getMessage()), e); - } - } - - private static final class CopyFileVisitor extends SimpleFileVisitor { - private final Path source; - private final Path target; - - CopyFileVisitor(Path source, Path target) { - this.source = source; - this.target = target; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - Path resolve = target.resolve(source.relativize(dir)); - if (Files.notExists(resolve)) { - Files.createDirectories(resolve); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Path resolve = target.resolve(source.relativize(file)); - Files.copy(file, resolve, StandardCopyOption.REPLACE_EXISTING); - return FileVisitResult.CONTINUE; - - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) { - return FileVisitResult.TERMINATE; - } - } -} +} \ No newline at end of file