package org.openslx.taskmanager.tasks; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.openslx.satserver.util.Exec; import org.openslx.satserver.util.Exec.ExecCallback; import org.openslx.satserver.util.Util; import org.openslx.taskmanager.api.AbstractTask; import com.google.gson.annotations.Expose; /** * Task for enabling or disabling https support in lighttpd. * Can greate a self-signed cert on the fly, or use a supplied one. */ public class LighttpdHttps extends AbstractTask { private Output status = new Output(); // --- User-supplied cert --- @Expose private String importcert = null; @Expose private String importkey = null; @Expose private String importchain = null; // --- Let's encrypt or similar (ACME) --- @Expose private String acmeMode = null; @Expose private String acmeMail = null; @Expose private String[] acmeDomains = null; @Expose private String acmeProvider = null; @Expose private boolean acmeWipeAll = false; @Expose private String acmeKeyId = null; @Expose private String acmeHmacKey = null; // ---- Self-signed ---- /** IP address to put in self-signed cert */ @Expose private String proxyip = null; // ------ Force HTTPS? ------ /** Enable redirect from HTTP to HTTPS? */ @Expose private boolean redirect; /** Only set the requested redirect mode, nothing else */ @Expose private boolean redirectOnly; // ------- private static List BASE_CMD = Collections.unmodifiableList( Arrays.asList( new String[] { "sudo", "-n", "-u", "root", "/opt/taskmanager/scripts/install-https" } ) ); @Override protected boolean initTask() { this.setStatusObject( this.status ); return true; } @Override protected boolean execute() { if ( this.redirectOnly ) return setRedirect(); if ( this.importcert != null && this.importkey != null && !this.importcert.isEmpty() && !this.importkey.isEmpty() ) return createFromInput(); if ( this.acmeMode != null ) return handleAcme(); if ( this.proxyip != null && !this.proxyip.isEmpty() ) return createRandom(); return disableHttps(); } /** * Create a new self-signed certificate and use that. */ private boolean createRandom() { List cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } cmd.add( "--random" ); cmd.add( this.proxyip ); int ret = Exec.sync( 45, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "generator exited with code " + ret ); return false; } return true; } /** * Create HTTPS config from cert/key/chain provided in task data. */ private boolean createFromInput() { // Import supplied certificate and key. Test if they are valid first File tmpKey = null; File tmpCert = null; File tmpChain = null; try { try { tmpCert = File.createTempFile( "bwlp-", ".pem" ); tmpKey = File.createTempFile( "bwlp-", ".pem" ); Util.writeStringToFile( tmpCert, this.importcert ); Util.writeStringToFile( tmpKey, this.importkey ); if ( this.importchain != null && !this.importchain.isEmpty() ) { tmpChain = File.createTempFile( "bwlp-", ".pem" ); Util.writeStringToFile( tmpChain, this.importchain ); } } catch ( Exception e ) { status.processStdOut( "Could not create temporary files: " + e.getMessage() ); return false; } return createFromFiles( tmpKey, tmpCert, tmpChain ); } finally { if ( tmpKey != null ) { tmpKey.delete(); } if ( tmpCert != null ) { tmpCert.delete(); } if ( tmpChain != null ) { tmpChain.delete(); } } } /** * Call deployment script, passing along predefined key/cert/chain files. */ private boolean createFromFiles( File tmpKey, File tmpCert, File tmpChain ) { List cmd; int ret; cmd = new ArrayList<>( BASE_CMD ); cmd.add( "--test" ); cmd.add( tmpKey.getAbsolutePath() ); cmd.add( tmpCert.getAbsolutePath() ); ret = Exec.sync( 45, status, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "Given key and certificate do not match, or have invalid format (exit code: " + ret + ")" ); return false; } cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } cmd.add( "--import" ); cmd.add( tmpKey.getAbsolutePath() ); cmd.add( tmpCert.getAbsolutePath() ); if ( tmpChain != null ) { cmd.add( tmpChain.getAbsolutePath() ); } ret = Exec.sync( 45, status, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "import exited with code " + ret ); return false; } return true; } private boolean handleAcme() { if ( this.acmeMode.equals( "issue" ) ) { return createWithAcme(); } if ( this.acmeMode.equals( "renew" ) ) { return checkRenewAcme(); } if ( this.acmeMode.equals( "try-enable" ) ) { return tryEnableAcme(); } status.processStdOut( "Invalid ACME mode: " + this.acmeMode ); return false; } /** * Create a brand new certificate via ACME */ private boolean createWithAcme() { if ( Util.isEmpty( this.acmeMail ) ) { status.processStdOut( "Invalid E-Mail provided" ); return false; } if ( this.acmeDomains == null || this.acmeDomains.length == 0 ) { status.processStdOut( "No domains provided" ); return false; } List cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } if ( this.acmeWipeAll ) { cmd.add( "--acme-wipe" ); } if ( this.acmeKeyId != null ) { cmd.add( "--acme-key-id" ); cmd.add( this.acmeKeyId ); } if ( this.acmeHmacKey != null ) { cmd.add( "--acme-hmac-key" ); cmd.add( this.acmeHmacKey ); } cmd.add( "--acme-issue" ); cmd.add( this.acmeProvider ); cmd.add( this.acmeMail ); for ( String d : this.acmeDomains ) { cmd.add( d ); } int ret = Exec.sync( 120, status, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "acme issue exited with code " + ret ); return false; } return true; } /** * Trigger a renew check and according renew if required. */ private boolean checkRenewAcme() { if ( this.acmeDomains == null || this.acmeDomains.length == 0 ) { status.processStdOut( "No domain provided" ); return false; } List cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } cmd.add( "--acme-renew" ); cmd.add( this.acmeDomains[0] ); // Only needs primary domain to find config int ret = Exec.sync( 120, status, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "acme renew exited with code " + ret ); return false; } return true; } private boolean tryEnableAcme() { List cmd = new ArrayList<>( BASE_CMD ); if ( this.redirect ) { cmd.add( "--redirect" ); } cmd.add( "--acme-try-enable" ); int ret = Exec.sync( 10, status, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "acme try-enable exited with code " + ret ); return false; } return true; } private boolean setRedirect() { List cmd = new ArrayList<>( BASE_CMD ); cmd.add( "--redirect-only" ); if ( this.redirect ) { cmd.add( "--redirect" ); } int ret = Exec.sync( 10, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "set redirect exited with code " + ret ); return false; } return true; } private boolean disableHttps() { List cmd = new ArrayList<>( BASE_CMD ); cmd.add( "--disable" ); int ret = Exec.sync( 10, cmd.toArray( new String[ cmd.size() ] ) ); if ( ret != 0 ) { status.processStdOut( "disable exited with code " + ret ); return false; } return true; } /** * Output - contains additional status data of this task */ private static class Output implements ExecCallback { protected StringBuilder error; @Override public void processStdOut( String line ) { if ( error == null ) { error = new StringBuilder(); } error.append( line ); error.append( '\n' ); } @Override public void processStdErr( String line ) { processStdOut( line ); } } }