001/**
002 * Copyright 2010-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.common.util.channel.model;
017
018import java.io.File;
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Properties;
022
023import org.kuali.common.util.Assert;
024import org.kuali.common.util.CollectionUtils;
025import org.kuali.common.util.LocationUtils;
026import org.kuali.common.util.channel.util.SSHUtils;
027import org.kuali.common.util.enc.EncUtils;
028import org.kuali.common.util.enc.EncryptionService;
029import org.kuali.common.util.nullify.NullUtils;
030import org.kuali.common.util.property.ImmutableProperties;
031import org.kuali.common.util.spring.SpringUtils;
032import org.kuali.common.util.spring.env.EnvUtils;
033import org.kuali.common.util.spring.env.EnvironmentService;
034
035import com.google.common.base.Charsets;
036import com.google.common.base.Optional;
037import com.google.common.collect.ImmutableList;
038
039public final class ChannelContext {
040
041        private final Optional<String> username;
042        private final String hostname;
043        private final List<String> privateKeys;
044        private final int port;
045        private final String encoding;
046        private final boolean strictHostKeyChecking;
047        private final boolean requestPseudoTerminal;
048        private final Optional<Integer> connectTimeout;
049        private final Properties options;
050        private final File knownHosts;
051        private final File config;
052        private final boolean useConfigFile;
053        private final boolean useKnownHosts;
054        private final boolean includeDefaultPrivateKeyLocations;
055        private final int waitForClosedSleepMillis;
056        private final List<File> privateKeyFiles;
057        private final boolean echo;
058
059        public static class Builder {
060
061                // Required
062                private final String hostname;
063
064                // Optional
065                private Optional<String> username = Optional.absent();
066                private int port = 22;
067                private Optional<Integer> connectTimeout = Optional.absent();
068                private String encoding = Charsets.UTF_8.name();
069                private Properties options = ImmutableProperties.of();
070                private boolean strictHostKeyChecking = false;
071                private boolean requestPseudoTerminal = false;
072                private File knownHosts = SSHUtils.DEFAULT_KNOWN_HOSTS;
073                private boolean useKnownHosts = false;
074                private File config = SSHUtils.DEFAULT_CONFIG_FILE;
075                private boolean useConfigFile = false;
076                private boolean includeDefaultPrivateKeyLocations = false;
077                private int waitForClosedSleepMillis = 10; // number of milliseconds to sleep when looping to see if an exec'd command has finished
078                private List<File> privateKeyFiles = ImmutableList.of();
079                private List<String> privateKeys = ImmutableList.of();
080                private boolean echo = true;
081
082                // Used only by the builder
083                private final Optional<EnvironmentService> env;
084                private final Optional<EncryptionService> enc;
085                private static final String HOSTNAME_KEY = "ssh.hostname";
086                private static final String USERNAME_KEY = "ssh.username";
087                private static final String REQUEST_PSEUDO_TERMINAL_KEY = "ssh.requestPseudoTerminal";
088                private static final String PRIVATE_KEYS_KEY = "ssh.privateKeys";
089                private static final String ECHO_KEY = "ssh.echo";
090                private static final String PORT_KEY = "ssh.port";
091                private static final String ENCODING_KEY = "ssh.encoding";
092                private static final String CONNECT_TIMEOUT_KEY = "ssh.connectTimeout";
093
094                /**
095                 * 
096                 */
097                public Builder(String hostname) {
098                        this(EnvUtils.ABSENT, EncUtils.ABSENT, hostname);
099                }
100
101                /**
102                 * Override using <code>ssh.hostname</code> (if present)
103                 */
104                public Builder(EnvironmentService env, String hostname) {
105                        this(Optional.of(env), EncUtils.ABSENT, hostname);
106                }
107
108                /**
109                 * Override using <code>ssh.hostname</code> (if present)
110                 */
111                public Builder(EnvironmentService env, EncryptionService enc, String hostname) {
112                        this(Optional.of(env), Optional.of(enc), hostname);
113                }
114
115                /**
116                 * Override using <code>ssh.hostname</code> (if present)
117                 */
118                private Builder(Optional<EnvironmentService> env, Optional<EncryptionService> enc, String hostname) {
119                        if (env.isPresent()) {
120                                this.hostname = env.get().getString(HOSTNAME_KEY, hostname);
121                        } else {
122                                this.hostname = hostname;
123                        }
124                        this.env = env;
125                        this.enc = enc;
126                }
127
128                public Builder requestPseudoTerminal(boolean requestPseudoTerminal) {
129                        this.requestPseudoTerminal = requestPseudoTerminal;
130                        return this;
131                }
132
133                public Builder echo(boolean echo) {
134                        this.echo = echo;
135                        return this;
136                }
137
138                public Builder username(String username) {
139                        this.username = NullUtils.toAbsent(username);
140                        return this;
141                }
142
143                public Builder port(int port) {
144                        this.port = port;
145                        return this;
146                }
147
148                public Builder encoding(String encoding) {
149                        this.encoding = encoding;
150                        return this;
151                }
152
153                public Builder connectTimeout(int connectTimeout) {
154                        this.connectTimeout = Optional.of(connectTimeout);
155                        return this;
156                }
157
158                public Builder options(Properties options) {
159                        this.options = options;
160                        return this;
161                }
162
163                public Builder knownHosts(File knownHosts) {
164                        this.knownHosts = knownHosts;
165                        return this;
166                }
167
168                public Builder useKnownHosts(boolean useKnownHosts) {
169                        this.useKnownHosts = useKnownHosts;
170                        return this;
171                }
172
173                public Builder config(File config) {
174                        this.config = config;
175                        return this;
176                }
177
178                public Builder useConfigFile(boolean useConfigFile) {
179                        this.useConfigFile = useConfigFile;
180                        return this;
181                }
182
183                public Builder includeDefaultPrivateKeyLocations(boolean includeDefaultPrivateKeyLocations) {
184                        this.includeDefaultPrivateKeyLocations = includeDefaultPrivateKeyLocations;
185                        return this;
186                }
187
188                public Builder waitForClosedSleepMillis(int waitForClosedSleepMillis) {
189                        this.waitForClosedSleepMillis = waitForClosedSleepMillis;
190                        return this;
191                }
192
193                public Builder privateKeyFiles(List<File> privateKeyFiles) {
194                        this.privateKeyFiles = privateKeyFiles;
195                        return this;
196                }
197
198                public Builder privateKey(String privateKey) {
199                        Optional<String> trimmed = NullUtils.toAbsent(privateKey);
200                        if (trimmed.isPresent()) {
201                                return privateKeys(ImmutableList.of(trimmed.get()));
202                        } else {
203                                return privateKeys(ImmutableList.<String> of());
204                        }
205                }
206
207                public Builder privateKeys(List<String> privateKeys) {
208                        this.privateKeys = privateKeys;
209                        return this;
210                }
211
212                /**
213                 * Override provided values with values from the environment
214                 */
215                private void override() {
216                        if (env.isPresent()) {
217                                username(SpringUtils.getString(env.get(), USERNAME_KEY, username).orNull());
218                                requestPseudoTerminal(env.get().getBoolean(REQUEST_PSEUDO_TERMINAL_KEY, requestPseudoTerminal));
219                                privateKeys(SpringUtils.getStrings(env.get(), PRIVATE_KEYS_KEY, privateKeys));
220                                echo(env.get().getBoolean(ECHO_KEY, echo));
221                                port(env.get().getInteger(PORT_KEY, port));
222                                encoding(env.get().getString(ENCODING_KEY, encoding));
223                                Optional<Integer> connectTimeout = SpringUtils.getProperty(env, CONNECT_TIMEOUT_KEY, Integer.class, this.connectTimeout);
224                                if (connectTimeout.isPresent()) {
225                                        connectTimeout(connectTimeout.get());
226                                }
227                        }
228                }
229
230                private void finish() {
231                        override();
232                        privateKeys(EncUtils.decrypt(enc, privateKeys));
233                        this.privateKeyFiles = ImmutableList.copyOf(getUniquePrivateKeyFiles(privateKeyFiles, useConfigFile, config, includeDefaultPrivateKeyLocations));
234                        this.privateKeys = ImmutableList.copyOf(privateKeys);
235                        this.options = ImmutableProperties.copyOf(getSessionProperties(options, strictHostKeyChecking));
236                }
237
238                private void validate(ChannelContext ctx) {
239                        Assert.noBlanks(ctx.getHostname(), ctx.getEncoding());
240                        Assert.noNulls(ctx.getUsername(), ctx.getConnectTimeout(), ctx.getOptions(), ctx.getKnownHosts(), ctx.getConfig(), ctx.getPrivateKeyFiles(), ctx.getPrivateKeys());
241                        Assert.isPort(ctx.getPort());
242                        Assert.positive(ctx.getWaitForClosedSleepMillis());
243                        Assert.notEncrypted(ctx.getPrivateKeys());
244                        if (ctx.isUseConfigFile()) {
245                                Assert.exists(ctx.getConfig());
246                                Assert.isTrue(ctx.getConfig().canRead(), "[" + ctx.getConfig() + "] exists but is not readable");
247                        }
248                        if (ctx.getConnectTimeout().isPresent()) {
249                                Assert.positive(ctx.getConnectTimeout().get());
250                        }
251                }
252
253                public ChannelContext build() {
254                        finish();
255                        ChannelContext ctx = new ChannelContext(this);
256                        validate(ctx);
257                        return ctx;
258                }
259
260                private List<File> getUniquePrivateKeyFiles(List<File> privateKeyFiles, boolean useConfigFile, File config, boolean includeDefaultPrivateKeyLocations) {
261                        List<String> paths = new ArrayList<String>();
262                        for (File privateKeyFile : privateKeyFiles) {
263                                paths.add(LocationUtils.getCanonicalPath(privateKeyFile));
264                        }
265                        if (useConfigFile) {
266                                for (String path : SSHUtils.getFilenames(config)) {
267                                        paths.add(path);
268                                }
269                        }
270                        if (includeDefaultPrivateKeyLocations) {
271                                for (String path : SSHUtils.PRIVATE_KEY_DEFAULTS) {
272                                        paths.add(path);
273                                }
274                        }
275                        List<String> uniquePaths = CollectionUtils.getUniqueStrings(paths);
276                        return SSHUtils.getExistingAndReadable(uniquePaths);
277                }
278
279                private Properties getSessionProperties(Properties options, boolean strictHostKeyChecking) {
280                        Properties properties = new Properties();
281                        properties.putAll(options);
282                        if (!strictHostKeyChecking) {
283                                properties.setProperty(SSHUtils.STRICT_HOST_KEY_CHECKING, SSHUtils.NO);
284                        }
285                        return properties;
286                }
287        }
288
289        private ChannelContext(Builder builder) {
290                this.username = builder.username;
291                this.hostname = builder.hostname;
292                this.port = builder.port;
293                this.encoding = builder.encoding;
294                this.connectTimeout = builder.connectTimeout;
295                this.options = builder.options;
296                this.strictHostKeyChecking = builder.strictHostKeyChecking;
297                this.requestPseudoTerminal = builder.requestPseudoTerminal;
298                this.knownHosts = builder.knownHosts;
299                this.config = builder.config;
300                this.useConfigFile = builder.useConfigFile;
301                this.includeDefaultPrivateKeyLocations = builder.includeDefaultPrivateKeyLocations;
302                this.waitForClosedSleepMillis = builder.waitForClosedSleepMillis;
303                this.privateKeyFiles = builder.privateKeyFiles;
304                this.privateKeys = builder.privateKeys;
305                this.useKnownHosts = builder.useKnownHosts;
306                this.echo = builder.echo;
307        }
308
309        public Optional<String> getUsername() {
310                return username;
311        }
312
313        public String getHostname() {
314                return hostname;
315        }
316
317        public int getPort() {
318                return port;
319        }
320
321        public Optional<Integer> getConnectTimeout() {
322                return connectTimeout;
323        }
324
325        public String getEncoding() {
326                return encoding;
327        }
328
329        public Properties getOptions() {
330                return options;
331        }
332
333        public boolean isStrictHostKeyChecking() {
334                return strictHostKeyChecking;
335        }
336
337        public boolean isRequestPseudoTerminal() {
338                return requestPseudoTerminal;
339        }
340
341        public File getKnownHosts() {
342                return knownHosts;
343        }
344
345        public File getConfig() {
346                return config;
347        }
348
349        public boolean isUseConfigFile() {
350                return useConfigFile;
351        }
352
353        public boolean isUseKnownHosts() {
354                return useKnownHosts;
355        }
356
357        public boolean isIncludeDefaultPrivateKeyLocations() {
358                return includeDefaultPrivateKeyLocations;
359        }
360
361        public int getWaitForClosedSleepMillis() {
362                return waitForClosedSleepMillis;
363        }
364
365        public List<File> getPrivateKeyFiles() {
366                return privateKeyFiles;
367        }
368
369        public List<String> getPrivateKeys() {
370                return privateKeys;
371        }
372
373        public boolean isEcho() {
374                return echo;
375        }
376
377}