Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@

package org.codehaus.plexus.components.secdispatcher;

import java.io.IOException;
import java.util.Map;
import java.util.Set;

import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;

/**
* This component decrypts a string, passed to it
*
Expand All @@ -26,7 +29,7 @@ public interface SecDispatcher {
* The default path of configuration.
* <p>
* The character {@code ~} (tilde) may be present as first character ONLY and is
* interpreted as "user home".
* interpreted as "user.home" system property, and it MUST be followed by path separator.
*/
String DEFAULT_CONFIGURATION = "~/.m2/settings-security.xml";

Expand All @@ -53,7 +56,7 @@ public interface SecDispatcher {
Set<String> availableCiphers();

/**
* encrypt given plaintext string
* Encrypt given plaintext string.
*
* @param str the plaintext to encrypt
* @param attr the attributes, may be {@code null}
Expand All @@ -63,11 +66,28 @@ public interface SecDispatcher {
String encrypt(String str, Map<String, String> attr) throws SecDispatcherException;

/**
* decrypt given encrypted string
* Decrypt given encrypted string.
*
* @param str the encrypted string
* @return plaintext string
* @return decrypted string
* @throws SecDispatcherException in case of problem
*/
String decrypt(String str) throws SecDispatcherException;

/**
* Reads the effective configuration, eventually creating new instance if not present.
*
* @param createIfMissing If {@code true}, it will create a new empty instance
* @return the configuration, of {@code null} if it does not exist in {@code createIfMissing} is {@code false}
* @throws IOException In case of IO problem
*/
SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException;

/**
* Writes the effective configuration.
*
* @param configuration The configuration to write, may not be {@code null}
* @throws IOException In case of IO problem
*/
void writeConfiguration(SettingsSecurity configuration) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import javax.inject.Named;
import javax.inject.Singleton;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -115,6 +118,21 @@ public String decrypt(String str) throws SecDispatcherException {
}
}

@Override
public SettingsSecurity readConfiguration(boolean createIfMissing) throws IOException {
SettingsSecurity configuration = SecUtil.read(getConfigurationPath());
if (configuration == null && createIfMissing) {
configuration = new SettingsSecurity();
}
return configuration;
}

@Override
public void writeConfiguration(SettingsSecurity configuration) throws IOException {
requireNonNull(configuration, "configuration is null");
SecUtil.writeWithBackup(getConfigurationPath(), configuration);
}

private Map<String, String> prepareDispatcherConfig(String type) {
HashMap<String, String> dispatcherConf = new HashMap<>();
SettingsSecurity sec = getConfiguration(false);
Expand Down Expand Up @@ -167,14 +185,22 @@ private boolean isEncryptedString(String str) {
return cipher.isEncryptedString(str);
}

private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException {
private Path getConfigurationPath() {
String location = System.getProperty(SYSTEM_PROPERTY_CONFIGURATION_LOCATION, getConfigurationFile());
location = location.charAt(0) == '~' ? System.getProperty("user.home") + location.substring(1) : location;
SettingsSecurity sec = SecUtil.read(location, true);
if (mandatory && sec == null)
throw new SecDispatcherException("Please check that configuration file on path " + location + " exists");
return Paths.get(location);
}

return sec;
private SettingsSecurity getConfiguration(boolean mandatory) throws SecDispatcherException {
Path path = getConfigurationPath();
try {
SettingsSecurity sec = SecUtil.read(path);
if (mandatory && sec == null)
throw new SecDispatcherException("Please check that configuration file on path " + path + " exists");
return sec;
} catch (IOException e) {
throw new SecDispatcherException(e.getMessage(), e);
}
}

private String getMasterPassword(SettingsSecurity sec, boolean mandatory) throws SecDispatcherException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,24 @@

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
import org.codehaus.plexus.components.secdispatcher.model.Config;
import org.codehaus.plexus.components.secdispatcher.model.ConfigProperty;
import org.codehaus.plexus.components.secdispatcher.model.SettingsSecurity;
import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxReader;
import org.codehaus.plexus.components.secdispatcher.model.io.stax.SecurityConfigurationStaxWriter;

import static java.util.Objects.requireNonNull;

Expand All @@ -40,44 +45,25 @@
* @version $Id$
*
*/
public class SecUtil {
public final class SecUtil {
private SecUtil() {}

public static final String PROTOCOL_DELIM = "://";
public static final int PROTOCOL_DELIM_LEN = PROTOCOL_DELIM.length();
public static final String[] URL_PROTOCOLS =
new String[] {"http", "https", "dav", "file", "davs", "webdav", "webdavs", "dav+http", "dav+https"};

public static SettingsSecurity read(String location, boolean cycle) throws SecDispatcherException {
if (location == null) throw new SecDispatcherException("location to read from is null");
/**
* Reads the configuration model up, optionally resolving relocation too.
*/
public static SettingsSecurity read(Path configurationFile) throws IOException {
requireNonNull(configurationFile, "configurationFile must not be null");
SettingsSecurity sec;
try {
try (InputStream in = toStream(location)) {
try (InputStream in = Files.newInputStream(configurationFile)) {
sec = new SecurityConfigurationStaxReader().read(in);
}
if (cycle && sec.getRelocation() != null) return read(sec.getRelocation(), true);
return sec;
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new SecDispatcherException("IO Problem", e);
} catch (XMLStreamException e) {
throw new SecDispatcherException("Parsing error", e);
}
}

private static InputStream toStream(String resource) throws IOException {
requireNonNull(resource, "resource is null");
int ind = resource.indexOf(PROTOCOL_DELIM);
if (ind > 1) {
String protocol = resource.substring(0, ind);
resource = resource.substring(ind + PROTOCOL_DELIM_LEN);
for (String p : URL_PROTOCOLS) {
if (protocol.regionMatches(true, 0, p, 0, p.length())) {
return new URL(p + PROTOCOL_DELIM + resource).openStream();
}
}
throw new IOException("Parsing error", e);
}
return Files.newInputStream(Paths.get(resource));
}

public static Map<String, String> getConfig(SettingsSecurity sec, String name) {
Expand All @@ -102,4 +88,64 @@ public static Map<String, String> getConfig(SettingsSecurity sec, String name) {
}
return null;
}

public static void write(Path target, SettingsSecurity configuration) throws IOException {
requireNonNull(target, "file must not be null");
requireNonNull(configuration, "sec must not be null");
writeFile(target, configuration, false);
}

public static void writeWithBackup(Path target, SettingsSecurity configuration) throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern is usually to have writeFile(Path, SettingsSecurity) and writeFile(Path, SettingsSecutiry, boolean) methods public methods, the first one calling the second one. And I don't see any reference to the first method. If not called, maybe simply keep the one with the backup.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test UT uses the "no backup method", but will remove one extra method and apply pattern you propose

requireNonNull(target, "file must not be null");
requireNonNull(configuration, "sec must not be null");
writeFile(target, configuration, true);
}

private static final boolean IS_WINDOWS =
System.getProperty("os.name", "unknown").startsWith("Windows");

private static void writeFile(Path target, SettingsSecurity configuration, boolean doBackup) throws IOException {
requireNonNull(target, "target is null");
Path parent = requireNonNull(target.getParent(), "target must have parent");
Files.createDirectories(parent);
Path tempFile = parent.resolve(target.getFileName() + "."
+ Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");

configuration.setModelVersion(SecDispatcher.class.getPackage().getSpecificationVersion());
configuration.setModelEncoding(StandardCharsets.UTF_8.name());

try (OutputStream out = Files.newOutputStream(tempFile)) {
new SecurityConfigurationStaxWriter().write(out, configuration);
if (doBackup && Files.isRegularFile(target)) {
Files.copy(target, parent.resolve(target.getFileName() + ".bak"), StandardCopyOption.REPLACE_EXISTING);
}
if (IS_WINDOWS) {
copy(tempFile, target);
} else {
Files.move(tempFile, target, StandardCopyOption.REPLACE_EXISTING);
}
} catch (XMLStreamException e) {
throw new IOException("XML Processing error", e);
} finally {
Files.deleteIfExists(tempFile);
}
}

/**
* On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
*/
private static void copy(Path source, Path target) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
byte[] array = buffer.array();
try (InputStream is = Files.newInputStream(source);
OutputStream os = Files.newOutputStream(target)) {
while (true) {
int bytes = is.read(array);
if (bytes < 0) {
break;
}
os.write(array, 0, bytes);
}
}
}
}
Loading
Loading