move to standalone plugin

This commit is contained in:
2026-08-23 13:44:39 +02:00
parent f227d054b8
commit 4188bea061
65 changed files with 14093 additions and 77 deletions

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.util.logging.Slf4j
import nextflow.util.Duration
import javax.net.ssl.KeyManager
import java.nio.file.Path
import java.nio.file.Paths
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
/**
* Models the kubernetes cluster client configuration settings
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@EqualsAndHashCode
@CompileStatic
@Slf4j
class ClientConfig {
boolean verifySsl
String server
String namespace
/**
* k8s service account name
* https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
*/
String serviceAccount
String token
byte[] sslCert
byte[] clientCert
byte[] clientKey
KeyManager[] keyManagers
K8sRetryConfig retryConfig
/**
* Timeout when reading from Input stream when a connection is established to a resource.
* If the timeout expires before there is data available for read, a {@link java.net.SocketTimeoutException} is raised
*/
Duration httpReadTimeout
/**
* Timeout when opening a communications link to the resource referenced by K8sClient request connection
* If the timeout expires before there is data available for read, a {@link java.net.SocketTimeoutException} is raised
*/
Duration httpConnectTimeout
/**
* When true signal that the configuration was retrieved from within a K8s cluster
*/
boolean isFromCluster
String getNamespace() { namespace ?: 'default' }
ClientConfig() {
retryConfig = new K8sRetryConfig()
}
String toString() {
"${this.class.getSimpleName()}[ server=$server, namespace=$namespace, serviceAccount=$serviceAccount, token=${cut(token)}, sslCert=${cut(sslCert)}, clientCert=${cut(clientCert)}, clientKey=${cut(clientKey)}, verifySsl=$verifySsl, fromFile=$isFromCluster, httpReadTimeout=$httpReadTimeout, httpConnectTimeout=$httpConnectTimeout, retryConfig=$retryConfig ]"
}
private String cut(String str) {
if( !str ) return '-'
return str.size()<10 ? str : str[0..10].toString() + '..'
}
private String cut(byte[] bytes) {
if( !bytes ) return '-'
cut(bytes.encodeBase64().toString())
}
static ClientConfig discover(String context, String namespace, String serviceAccount) {
new ConfigDiscovery().discover(context, namespace, serviceAccount)
}
static ClientConfig fromNextflowConfig(Map opts, String namespace, String serviceAccount) {
final result = new ClientConfig()
if( opts.server )
result.server = opts.server
if( opts.token )
result.token = opts.token
else if( opts.tokenFile )
result.token = Paths.get(opts.tokenFile.toString()).getText('UTF-8')
result.namespace = namespace ?: opts.namespace ?: 'default'
result.serviceAccount = serviceAccount ?: 'default'
if( opts.verifySsl )
result.verifySsl = opts.verifySsl as boolean
if( opts.sslCert )
result.sslCert = opts.sslCert.toString().decodeBase64()
else if( opts.sslCertFile )
result.sslCert = Paths.get(opts.sslCertFile.toString()).bytes
if( opts.clientCert )
result.clientCert = opts.clientCert.toString().decodeBase64()
else if( opts.clientCertFile )
result.clientCert = Paths.get(opts.clientCertFile.toString()).bytes
if( opts.clientKey )
result.clientKey = opts.clientKey.toString().decodeBase64()
else if( opts.clientKeyFile )
result.clientKey = Paths.get(opts.clientKeyFile.toString()).bytes
return result
}
static ClientConfig fromUserAndCluster(Map user, Map cluster, Path location) {
final base = location.isDirectory() ? location : location.parent
final result = new ClientConfig()
if( user.token )
result.token = user.token
else if( user.tokenFile ) {
result.token = Paths.get(user.tokenFile.toString()).getText('UTF-8')
}
if( user."client-certificate" )
result.clientCert = base.resolve(user."client-certificate".toString()).bytes
else if( user."client-certificate-data" )
result.clientCert = user."client-certificate-data".toString().decodeBase64()
if( user."client-key" )
result.clientKey = base.resolve(user."client-key".toString()).bytes
else if( user."client-key-data" )
result.clientKey = user."client-key-data".toString().decodeBase64()
// -- cluster settings
if( cluster.server )
result.server = cluster.server
if( cluster."certificate-authority-data" )
result.sslCert = cluster."certificate-authority-data".toString().decodeBase64()
else if( cluster."certificate-authority" )
result.sslCert = base.resolve(cluster."certificate-authority".toString()).bytes
result.verifySsl = cluster."insecure-skip-tls-verify" != true
return result
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import javax.net.ssl.KeyManager
import javax.net.ssl.KeyManagerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.KeyStore
import static nextflow.util.StringUtils.formatHostName
import groovy.util.logging.Slf4j
import org.yaml.snakeyaml.Yaml
/**
* Discover Kubernetes configuration from system environment
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
class ConfigDiscovery {
private Map<String,String> env = System.getenv()
ConfigDiscovery() { }
/**
* Discover Kubernetes client configuration from current environment using
* either the .kube/config file or the pod service service account virtual
* file system when running in a pod.
*
* @param contextName The K8s config context name.
* @param namespace The K8s cluster namespace
* @param serviceAccount The K8s cluster service account
* @return The e
*/
ClientConfig discover(String contextName, String namespace, String serviceAccount) {
// Note: System.getProperty('user.home') may not report the correct home path when
// running in a container. Use env HOME instead.
def home = System.getenv('HOME')
def kubeConfig = env.get('KUBECONFIG') ? env.get('KUBECONFIG') : "$home/.kube/config"
def configFile = Paths.get(kubeConfig)
// determine the Kubernetes client configuration via the `.kube/config` file
if( configFile.exists() ) {
return fromKubeConfig(configFile, contextName, namespace, serviceAccount)
}
else {
log.debug "K8s config file does not exist: $configFile"
}
// determine the Kubernetes client configuration via the pod environment
if( env.get('KUBERNETES_SERVICE_HOST') ) {
return fromCluster(env, namespace, serviceAccount)
}
else {
log.debug "K8s env variable KUBERNETES_SERVICE_HOST is not defined"
}
throw new IllegalStateException("Unable to lookup Kubernetes cluster configuration")
}
protected ClientConfig fromCluster(Map<String,String> env, String cfgNamespace, String serviceAccount) {
// See https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#accessing-the-api-from-a-pod
final host = env.get('KUBERNETES_SERVICE_HOST')
final port = env.get('KUBERNETES_SERVICE_PORT')
final server = formatHostName(host, port)
final cert = path('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt').bytes
final token = path('/var/run/secrets/kubernetes.io/serviceaccount/token').text
final namespace = path('/var/run/secrets/kubernetes.io/serviceaccount/namespace').text
return new ClientConfig(
server: server,
token: token,
namespace: cfgNamespace ?: namespace,
serviceAccount: serviceAccount,
sslCert: cert,
isFromCluster: true )
}
protected Path path(String path) {
Paths.get(path)
}
protected ClientConfig fromKubeConfig(Path path, String contextName, String namespace, String serviceAccount) {
def yaml = (Map)new Yaml().load(Files.newInputStream(path))
contextName ?= yaml."current-context" as String
final allContext = yaml.contexts as List
final allClusters = yaml.clusters as List
final allUsers = yaml.users as List
final context = allContext.find { Map it -> it.name == contextName } ?.context
if( !context )
throw new IllegalArgumentException("Unknown Kubernetes context: $contextName -- check config file: $path")
final userName = context?.user
final clusterName = context?.cluster
final user = allUsers.find{ Map it -> it.name == userName } ?.user ?: [:]
final cluster = allClusters.find{ Map it -> it.name == clusterName } ?.cluster ?: [:]
final config = ClientConfig.fromUserAndCluster(user, cluster, path)
// the namespace provided should have priority over the context current namespace
config.namespace = namespace ?: context?.namespace ?: 'default'
config.serviceAccount = serviceAccount ?: 'default'
if( config.clientCert && config.clientKey ) {
config.keyManagers = createKeyManagers(config.clientCert, config.clientKey)
}
else if( !config.token ) {
config.token = discoverAuthToken(contextName, config.namespace, config.serviceAccount)
}
return config
}
protected KeyStore createKeyStore0(byte[] clientCert, byte[] clientKey, char[] passphrase, String alg) {
def cert = new ByteArrayInputStream(clientCert)
def key = new ByteArrayInputStream(clientKey)
return SSLUtils.createKeyStore(cert, key, alg, passphrase, null, null)
}
protected KeyStore createKeyStore(byte[] clientCert, byte[] clientKey, char[] passphrase) {
try {
// try first RSA algorithm
return createKeyStore0(clientCert, clientKey, passphrase, "RSA")
}
catch (Exception e1) {
// fallback to EC algorithm
try {
return createKeyStore0(clientCert, clientKey, passphrase, "EC")
}
catch (Exception e2) {
// if still fails, throws the first exception
throw e1
}
}
}
protected KeyManager[] createKeyManagers(byte[] clientCert, byte[] clientKey) {
final passphrase = "".toCharArray()
final keyStore = createKeyStore(clientCert, clientKey, passphrase)
final kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, passphrase);
return kmf.getKeyManagers();
}
String discoverAuthToken(String context, String namespace, String serviceAccount) {
context ?= 'default'
namespace ?= 'default'
serviceAccount ?= 'default'
final cmd = "kubectl --context $context -n ${namespace} get secret -o=jsonpath='{.items[?(@.metadata.annotations.kubernetes\\.io/service-account\\.name==\"$serviceAccount\")].data.token}'"
final proc = new ProcessBuilder('bash','-o','pipefail','-c', cmd).start()
final status = proc.waitFor()
final text = proc.inputStream?.text
if( status==0 && text ) {
try {
return new String(text.trim().decodeBase64())
}
catch( Exception e ) {
log.warn "Unable to decode K8s cluster auth token '$text' -- cause: ${e.message}"
}
}
else {
final cause = proc.errorStream?.text ?: text
final msg = cause ? "\n- cmd : $cmd\n- exit : $status\n- cause:\n${cause.indent(' ')}" : ''
log.warn "[K8s] unable to fetch auth token ${msg}"
}
return null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.transform.CompileStatic
/**
* Model a Kubernetes API response
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@CompileStatic
class K8sResponseApi {
private int code
private InputStream stream
private String text
K8sResponseApi(int code, InputStream stream) {
this.code = code
this.stream = stream
}
String toString() {
"code=$code; stream=$stream"
}
int getCode() { code }
InputStream getStream() { stream }
String getText() {
if( text == null ) {
text = stream?.text
}
return text
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
/**
* Model a kubernetes invalid response
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
@CompileStatic
class K8sResponseException extends Exception {
K8sResponseJson response
K8sResponseException(K8sResponseJson response) {
super(msg0(response))
this.response = response
}
K8sResponseException(String message, K8sResponseJson response) {
super(msg1(message,response))
this.response = response
}
K8sResponseException(String message, InputStream response) {
this(message, new K8sResponseJson(fetch(response)))
}
static private String msg1( String msg, K8sResponseJson resp ) {
if( !msg && resp==null )
return null
if( msg && resp != null ) {
def sep = resp.isRawText() ? ' -- ' : '\n'
return "${msg}${sep}${msg0(resp)}"
}
else if( msg ) {
return msg
}
else {
return msg0(resp)
}
}
static private String msg0( K8sResponseJson response ) {
if( response == null )
return null
if( response.isRawText() )
response.getRawText()
else
"\n${response.toString().indent(' ')}"
}
static private String fetch(InputStream stream) {
try {
return stream?.text
}
catch( Exception e ) {
log.debug "Unable to fetch response text -- Cause: ${e.message ?: e}"
return null
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
/**
* Model the response of a kubernetes api request
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@Slf4j
@CompileStatic
class K8sResponseJson implements Map {
@Delegate
private Map response
private String rawText
K8sResponseJson(Map response) {
this.response = response
}
K8sResponseJson(String response) {
this.response = toJson(response)
this.rawText = response
}
boolean isRawText() { !response && rawText }
String getRawText() { rawText }
static private Map toJson(String raw) {
try {
return (Map)new JsonSlurper().parseText(raw)
}
catch( Exception e ) {
log.trace "[K8s] cannot parse response to json -- raw: ${raw? '\n'+raw.indent(' ') :'null'}"
return Collections.emptyMap()
}
}
static private String prettyPrint(String json) {
try {
JsonOutput.prettyPrint(json)
}
catch( Exception e ) {
return json
}
}
String toString() {
response ? prettyPrint(JsonOutput.toJson(response)) : rawText
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import nextflow.config.spec.ConfigOption
import nextflow.config.spec.ConfigScope
import nextflow.script.dsl.Description
import nextflow.util.Duration
/**
* Model retry policy configuration
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@ToString(includePackage = false, includeNames = true)
@EqualsAndHashCode
@CompileStatic
class K8sRetryConfig implements ConfigScope {
@ConfigOption
@Description("""
Delay when retrying failed API requests (default: `250ms`).
""")
Duration delay = Duration.of('250ms')
@ConfigOption
@Description("""
Max delay when retrying failed API requests (default: `90s`).
""")
Duration maxDelay = Duration.of('90s')
@ConfigOption
@Description("""
Max attempts when retrying failed API requests (default: `4`).
""")
int maxAttempts = 4
@ConfigOption
@Description("""
Jitter value when retrying failed API requests (default: `0.25`).
""")
double jitter = 0.25
K8sRetryConfig() {
this(Collections.emptyMap())
}
K8sRetryConfig(Map config) {
if( config.delay )
delay = config.delay as Duration
if( config.maxDelay )
maxDelay = config.maxDelay as Duration
if( config.maxAttempts )
maxAttempts = config.maxAttempts as int
if( config.jitter )
jitter = config.jitter as double
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client
import groovy.transform.CompileStatic
import nextflow.exception.ProcessException
import nextflow.exception.ShowOnlyExceptionMessage
/**
* Exception raised when a pod cannot be scheduled because
* e.g. the container image cannot be pulled, required resources
* cannot be fulfilled, etc.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@CompileStatic
class PodUnschedulableException extends ProcessException implements ShowOnlyExceptionMessage {
PodUnschedulableException(String message, Throwable cause) {
super(message,cause)
}
}

View File

@@ -0,0 +1,318 @@
/*
* Copyright 2013-2026, Seqera Labs
*
* 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 recreationaltech.plugin.client;
/**
* This file is derived from
* https://github.com/kubernetes-client/java/blob/master/util/src/main/java/io/kubernetes/client/util/SSLUtils.java
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
public class SSLUtils {
public static boolean isNotNullOrEmpty(String val) {
return val != null && val.length() > 0;
}
public static KeyManager[] keyManagers(String certData, String certFile, String keyData, String keyFile,
String algo, String passphrase, String keyStoreFile, String keyStorePassphrase)
throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException, CertificateException,
InvalidKeySpecException, IOException {
KeyManager[] keyManagers = null;
if ((isNotNullOrEmpty(certData) || isNotNullOrEmpty(certFile))
&& (isNotNullOrEmpty(keyData) || isNotNullOrEmpty(keyFile))) {
KeyStore keyStore = createKeyStore(certData, certFile, keyData, keyFile, algo, passphrase, keyStoreFile,
keyStorePassphrase);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, passphrase.toCharArray());
keyManagers = kmf.getKeyManagers();
}
return keyManagers;
}
public static KeyStore createKeyStore(String clientCertData, String clientCertFile, String clientKeyData,
String clientKeyFile, String clientKeyAlgo, String clientKeyPassphrase, String keyStoreFile,
String keyStorePassphrase) throws IOException, CertificateException, NoSuchAlgorithmException,
InvalidKeySpecException, KeyStoreException {
try (InputStream certInputStream = getInputStreamFromDataOrFile(clientCertData, clientCertFile);
InputStream keyInputStream = getInputStreamFromDataOrFile(clientKeyData, clientKeyFile)) {
return createKeyStore(certInputStream, keyInputStream, clientKeyAlgo,
clientKeyPassphrase != null ? clientKeyPassphrase.toCharArray() : null,
keyStoreFile, getKeyStorePassphrase(keyStorePassphrase));
}
}
static private PrivateKey generateEcKey(InputStream keyInputStream) throws IOException {
PrivateKey privateKey=null;
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
Object object = new PEMParser(new InputStreamReader(keyInputStream)).readObject();
if (object instanceof PEMKeyPair) {
PEMKeyPair keys = (PEMKeyPair) object;
privateKey = new JcaPEMKeyConverter().getKeyPair(keys).getPrivate();
}
if( object instanceof PrivateKeyInfo) {
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo)object;
privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
}
if( privateKey == null) {
throw new IOException("Unsupported EC algorithm");
}
return privateKey;
}
static private PrivateKey generateStdKey(InputStream keyInputStream, String clientKeyAlgo) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
byte[] keyBytes = decodePem(keyInputStream);
KeyFactory keyFactory = KeyFactory.getInstance(clientKeyAlgo);
try {
// First let's try PKCS8
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
}
catch (InvalidKeySpecException e) {
// Otherwise try PKCS1
RSAPrivateCrtKeySpec keySpec = decodePKCS1(keyBytes);
return keyFactory.generatePrivate(keySpec);
}
}
public static KeyStore createKeyStore(InputStream certInputStream, InputStream keyInputStream, String clientKeyAlgo,
char[] clientKeyPassphrase, String keyStoreFile, char[] keyStorePassphrase) throws IOException,
CertificateException, NoSuchAlgorithmException, InvalidKeySpecException, KeyStoreException {
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(certInputStream);
PrivateKey privateKey = clientKeyAlgo.equals("EC")
? generateEcKey(keyInputStream)
: generateStdKey(keyInputStream, clientKeyAlgo);
KeyStore keyStore = KeyStore.getInstance("JKS");
if (keyStoreFile != null && keyStoreFile.length() > 0) {
keyStore.load(new FileInputStream(keyStoreFile), keyStorePassphrase);
} else {
loadDefaultKeyStoreFile(keyStore, keyStorePassphrase);
}
String alias = cert.getSubjectX500Principal().getName();
keyStore.setKeyEntry(alias, privateKey, clientKeyPassphrase, new Certificate[] { cert });
return keyStore;
}
// This method is inspired and partly taken over from
// http://oauth.googlecode.com/svn/code/java/
// All credits to belong to them.
private static byte[] decodePem(InputStream keyInputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(keyInputStream));
try {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("-----BEGIN ")) {
return readBytes(reader, line.trim().replace("BEGIN", "END"));
}
}
throw new IOException("PEM is invalid: no begin marker");
} finally {
reader.close();
}
}
private static byte[] readBytes(BufferedReader reader, String endMarker) throws IOException {
String line;
StringBuffer buf = new StringBuffer();
while ((line = reader.readLine()) != null) {
if (line.indexOf(endMarker) != -1) {
return Base64.decodeBase64(buf.toString());
}
buf.append(line.trim());
}
throw new IOException("PEM is invalid : No end marker");
}
public static RSAPrivateCrtKeySpec decodePKCS1(byte[] keyBytes) throws IOException {
DerParser parser = new DerParser(keyBytes);
Asn1Object sequence = parser.read();
sequence.validateSequence();
parser = new DerParser(sequence.getValue());
parser.read();
return new RSAPrivateCrtKeySpec(next(parser), next(parser), next(parser), next(parser), next(parser),
next(parser), next(parser), next(parser));
}
private static BigInteger next(DerParser parser) throws IOException {
return parser.read().getInteger();
}
static class DerParser {
private InputStream in;
DerParser(byte[] bytes) throws IOException {
this.in = new ByteArrayInputStream(bytes);
}
Asn1Object read() throws IOException {
int tag = in.read();
if (tag == -1) {
throw new IOException("Invalid DER: stream too short, missing tag");
}
int length = getLength();
byte[] value = new byte[length];
if (in.read(value) < length) {
throw new IOException("Invalid DER: stream too short, missing value");
}
return new Asn1Object(tag, value);
}
private int getLength() throws IOException {
int i = in.read();
if (i == -1) {
throw new IOException("Invalid DER: length missing");
}
if ((i & ~0x7F) == 0) {
return i;
}
int num = i & 0x7F;
if (i >= 0xFF || num > 4) {
throw new IOException("Invalid DER: length field too big (" + i + ")");
}
byte[] bytes = new byte[num];
if (in.read(bytes) < num) {
throw new IOException("Invalid DER: length too short");
}
return new BigInteger(1, bytes).intValue();
}
}
static class Asn1Object {
private final int type;
private final byte[] value;
private final int tag;
public Asn1Object(int tag, byte[] value) {
this.tag = tag;
this.type = tag & 0x1F;
this.value = value;
}
public byte[] getValue() {
return value;
}
BigInteger getInteger() throws IOException {
if (type != 0x02) {
throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$
}
return new BigInteger(value);
}
void validateSequence() throws IOException {
if (type != 0x10) {
throw new IOException("Invalid DER: not a sequence");
}
if ((tag & 0x20) != 0x20) {
throw new IOException("Invalid DER: can't parse primitive entity");
}
}
}
private static void loadDefaultKeyStoreFile(KeyStore keyStore, char[] keyStorePassphrase)
throws CertificateException, NoSuchAlgorithmException, IOException {
String keyStorePath = System.getProperty("javax.net.ssl.keyStore");
if (keyStorePath != null && keyStorePath.length() > 0) {
File keyStoreFile = new File(keyStorePath);
if (loadDefaultStoreFile(keyStore, keyStoreFile, keyStorePassphrase)) {
return;
}
}
keyStore.load(null);
}
private static boolean loadDefaultStoreFile(KeyStore keyStore, File fileToLoad, char[] passphrase)
throws CertificateException, NoSuchAlgorithmException, IOException {
if (fileToLoad.exists() && fileToLoad.isFile() && fileToLoad.length() > 0) {
keyStore.load(new FileInputStream(fileToLoad), passphrase);
return true;
}
return false;
}
public static InputStream getInputStreamFromDataOrFile(String data, String file) throws FileNotFoundException {
if (data != null) {
byte[] bytes = Base64.decodeBase64(data);
// TODO handle non-base64 here?
return new ByteArrayInputStream(bytes);
}
if (file != null) {
return new FileInputStream(file);
}
return null;
}
private static char[] getKeyStorePassphrase(String keyStorePassphrase) {
if (keyStorePassphrase == null || keyStorePassphrase.length() == 0) {
return System.getProperty("javax.net.ssl.keyStorePassword", "changeit").toCharArray();
}
return keyStorePassphrase.toCharArray();
}
}