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}