blob: 7c3043adc28b9c6f83d607e0897702e98facc47a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.docker;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.netbeans.modules.docker.api.Credentials;
import org.openide.filesystems.FileAttributeEvent;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileUtil;
import org.openide.util.Pair;
/**
*
* @author Petr Hejl
*/
public final class DockerConfig {
private static final String DOCKER_HUB_DOMAIN = "index.docker.io";
private static final String DOCKER_HUB_REGISTRY = "https://" + DOCKER_HUB_DOMAIN + "/v1/";
private static DockerConfig instance;
private final ConfigListener listener = new ConfigListener();
// GuardedBy("this")
private JSONObject auths;
// GuardedBy("this")
private Map<String, String> httpHeaders;
private DockerConfig() {
super();
}
public static synchronized DockerConfig getDefault() {
if (instance == null) {
instance = new DockerConfig();
instance.init();
}
return instance;
}
public List<Credentials> getAllCredentials() throws IOException {
JSONObject currentAuths;
synchronized (this) {
loadCache();
currentAuths = auths;
}
List<Credentials> ret = new ArrayList<>(currentAuths.size());
for (Iterator<Map.Entry> it = currentAuths.entrySet().iterator(); it.hasNext();) {
Map.Entry e = it.next();
if (!(e.getKey() instanceof String)) {
continue;
}
String registry = (String) e.getKey();
JSONObject value = (JSONObject) e.getValue();
if (value == null) {
continue;
}
ret.add(createCredentials(registry, value)); // NOI18N
}
return ret;
}
public Credentials getCredentials(String registry) throws IOException {
JSONObject currentAuths;
synchronized (this) {
loadCache();
currentAuths = auths;
}
Set<String> names = generateRegistryNames(registry);
JSONObject value = null;
Iterator<String> it = names.iterator();
while (value == null && it.hasNext()) {
value = (JSONObject) currentAuths.get(it.next());
}
if (value == null) {
return null;
}
return createCredentials(registry, value);
}
public Map<String, String> getHttpHeaders() throws IOException {
synchronized (this) {
loadCache();
return new HashMap<>(httpHeaders);
}
}
public void setCredentials(Credentials credentials) throws IOException {
StringBuilder sb = new StringBuilder(credentials.getUsername());
sb.append(':');
sb.append(credentials.getPassword());
String auth = Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8));
JSONObject value = new JSONObject();
value.put("auth", auth); // NOI18N
value.put("email", credentials.getEmail()); // NOI18N
Pair<File, Boolean> fileDesc = getConfigFile();
synchronized (this) {
try (RandomAccessFile f = new RandomAccessFile(fileDesc.first(), "rw")) {
try (FileChannel ch = f.getChannel()) {
try (FileLock lock = ch.lock()) {
JSONObject current = null;
if (f.length() > 0) {
current = (JSONObject) new JSONParser().parse(Channels.newReader(ch, "UTF-8"));
}
if (current == null) {
current = new JSONObject();
}
JSONObject currentAuths = current;
if (!fileDesc.second()) {
currentAuths = (JSONObject) current.get("auths");
if (currentAuths == null) {
currentAuths = new JSONObject();
current.put("auths", currentAuths);
}
}
currentAuths.put(credentials.getRegistry(), value);
ch.truncate(0);
Writer w = Channels.newWriter(ch, "UTF-8");
current.writeJSONString(w);
w.flush();
} catch (ParseException ex) {
throw new IOException(ex);
}
}
} finally {
clearCache();
}
}
}
public void removeCredentials(Credentials credentials) throws IOException {
Pair<File, Boolean> fileDesc = getConfigFile();
synchronized (this) {
try (RandomAccessFile f = new RandomAccessFile(fileDesc.first(), "rw")) {
try (FileChannel ch = f.getChannel()) {
try (FileLock lock = ch.lock()) {
JSONObject current = (JSONObject) new JSONParser().parse(Channels.newReader(ch, "UTF-8"));
if (f.length() <= 0) {
return;
}
JSONObject currentAuths = current;
if (!fileDesc.second()) {
currentAuths = (JSONObject) current.get("auths");
if (currentAuths == null) {
return;
}
}
currentAuths.remove(credentials.getRegistry());
ch.truncate(0);
Writer w = Channels.newWriter(ch, "UTF-8");
current.writeJSONString(w);
w.flush();
} catch (ParseException ex) {
throw new IOException(ex);
}
}
} finally {
clearCache();
}
}
}
private void init() {
FileUtil.addFileChangeListener(listener, getNewConfigFile());
FileUtil.addFileChangeListener(listener, getOldConfigFile());
}
private void loadCache() throws IOException {
synchronized (this) {
if (auths == null || httpHeaders == null) {
Pair<JSONObject, Boolean> parsed = parse();
if (parsed == null) {
auths = new JSONObject();
httpHeaders = new JSONObject();
return;
}
JSONObject currentAuths;
currentAuths = parsed.first();
if (!parsed.second()) {
// using the new config.json
currentAuths = (JSONObject) currentAuths.get("auths"); // NOI18N
}
if (currentAuths == null) {
currentAuths = new JSONObject();
}
auths = currentAuths;
JSONObject currentHeaders = null;
if (!parsed.second()) {
currentHeaders = (JSONObject) parsed.first().get("HttpHeaders"); // NOI18N
}
if (currentHeaders == null) {
currentHeaders = new JSONObject();
}
httpHeaders = new HashMap<>();
for (Iterator<Map.Entry> it = currentHeaders.entrySet().iterator(); it.hasNext(); ) {
Map.Entry e = it.next();
httpHeaders.put((String) e.getKey(), (String) e.getValue());
}
}
}
}
private void clearCache() {
synchronized (this) {
auths = null;
httpHeaders = null;
}
}
private Pair<JSONObject, Boolean> parse() throws IOException {
Pair<File, Boolean> fileDesc = getConfigFile();
synchronized (this) {
if (fileDesc.first().isFile()) {
try (FileInputStream is = new FileInputStream(fileDesc.first())) {
try (FileLock lock = is.getChannel().lock(0, Long.MAX_VALUE, true)) {
Reader r = new InputStreamReader(new BufferedInputStream(is), StandardCharsets.UTF_8);
JSONObject current = null;
if (fileDesc.first().length() > 0) {
current = (JSONObject) new JSONParser().parse(r);
}
if (current == null) {
return null;
}
return Pair.of(current, fileDesc.second());
} catch (ParseException ex) {
throw new IOException(ex);
}
}
}
return null;
}
}
private static Credentials createCredentials(String registry, JSONObject value) throws IOException {
if (value == null) {
return null;
}
byte[] auth = Base64.getDecoder().decode((String) value.get("auth")); // NOI18N
CharBuffer chars = StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(auth));
int index = -1;
for (int i = 0; i < chars.length(); i++) {
if (chars.get(i) == ':') {
index = i;
break;
}
}
if (index < 0) {
throw new IOException("Malformed registry authentication record");
}
String username = new String(chars.array(), 0, index);
char[] password = new char[chars.length() - index - 1];
if (password.length > 0) {
System.arraycopy(chars.array(), index + 1, password, 0, password.length);
}
return new Credentials(registry, username, password, (String) value.get("email")); // NOI18N
}
private static Pair<File, Boolean> getConfigFile() {
File newFile = getNewConfigFile();
boolean oldConfig = false;
File file = newFile;
if (!file.isFile()) {
file = getOldConfigFile();
oldConfig = true;
}
if (!file.isFile()) {
// what to do now ?
// FIXME test the client version; since 1.7 use the new format
file = newFile;
oldConfig = false;
}
return Pair.of(file, oldConfig);
}
private static File getNewConfigFile() {
String configPath = System.getenv("DOCKER_CONFIG");
if (configPath == null) {
configPath = System.getProperty("user.home") + File.separatorChar + ".docker";
}
return new File(configPath, "config.json");
}
private static File getOldConfigFile() {
return new File(System.getProperty("user.home"), ".dockercfg");
}
private static Set<String> generateRegistryNames(String registry) {
Set<String> result = new LinkedHashSet<>();
if (registry == null) {
result.add(DOCKER_HUB_REGISTRY);
result.addAll(generateRegistryNames(DOCKER_HUB_DOMAIN));
return result;
}
result.add(registry);
generatePaths(registry, result);
if (!registry.contains("://")) {
String https = "https://" + registry;
result.add(https);
generatePaths(https, result);
String http = "http://" + registry;
result.add(http);
generatePaths(http, result);
}
return result;
}
private static void generatePaths(String registry, Set<String> result) {
StringBuilder extended = new StringBuilder(registry);
if (registry.endsWith("/")) {
extended.append("v1");
} else {
extended.append("/v1");
}
result.add(extended.toString());
extended.append("/");
result.add(extended.toString());
}
private class ConfigListener implements FileChangeListener {
@Override
public void fileDataCreated(FileEvent fe) {
clearCache();
}
@Override
public void fileChanged(FileEvent fe) {
clearCache();
}
@Override
public void fileDeleted(FileEvent fe) {
clearCache();
}
@Override
public void fileRenamed(FileRenameEvent fe) {
clearCache();
}
@Override
public void fileAttributeChanged(FileAttributeEvent fe) {
// noop
}
@Override
public void fileFolderCreated(FileEvent fe) {
// noop
}
}
}