embulk-output-ftp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +3 -0
- data/README.md +98 -0
- data/build.gradle +80 -0
- data/classpath/bcpkix-jdk15on-1.52.jar +0 -0
- data/classpath/bcprov-jdk15on-1.52.jar +0 -0
- data/classpath/embulk-output-ftp-0.1.0.jar +0 -0
- data/classpath/ftp4j-1.7.2.jar +0 -0
- data/config/checkstyle/checkstyle.xml +128 -0
- data/config/checkstyle/default.xml +108 -0
- data/embulk-output-ftp.gemspec +18 -0
- data/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/gradle/wrapper/gradle-wrapper.properties +6 -0
- data/gradlew +160 -0
- data/gradlew.bat +90 -0
- data/lib/embulk/output/ftp.rb +3 -0
- data/libs/ftp4j-1.7.2.jar +0 -0
- data/src/main/java/org/embulk/output/ftp/BlockingTransfer.java +277 -0
- data/src/main/java/org/embulk/output/ftp/FtpFileOutputPlugin.java +456 -0
- data/src/main/java/org/embulk/output/ftp/SSLPlugins.java +260 -0
- data/src/main/java/org/embulk/output/ftp/TrustManagers.java +285 -0
- data/src/test/java/org/embulk/output/ftp/TestBlockingTransfer.java +5 -0
- data/src/test/java/org/embulk/output/ftp/TestFtpFileOutputPlugin.java +5 -0
- data/src/test/java/org/embulk/output/ftp/TestSSLPlugins.java +5 -0
- data/src/test/java/org/embulk/output/ftp/TrustedManagers.java +5 -0
- metadata +98 -0
@@ -0,0 +1,456 @@
|
|
1
|
+
package org.embulk.output.ftp;
|
2
|
+
|
3
|
+
import com.google.common.base.Optional;
|
4
|
+
import com.google.common.base.Throwables;
|
5
|
+
import it.sauronsoftware.ftp4j.FTPAbortedException;
|
6
|
+
import it.sauronsoftware.ftp4j.FTPClient;
|
7
|
+
import it.sauronsoftware.ftp4j.FTPCommunicationListener;
|
8
|
+
import it.sauronsoftware.ftp4j.FTPConnector;
|
9
|
+
import it.sauronsoftware.ftp4j.FTPDataTransferException;
|
10
|
+
import it.sauronsoftware.ftp4j.FTPDataTransferListener;
|
11
|
+
import it.sauronsoftware.ftp4j.FTPException;
|
12
|
+
import it.sauronsoftware.ftp4j.FTPIllegalReplyException;
|
13
|
+
import org.embulk.config.Config;
|
14
|
+
import org.embulk.config.ConfigDefault;
|
15
|
+
import org.embulk.config.ConfigDiff;
|
16
|
+
import org.embulk.config.ConfigException;
|
17
|
+
import org.embulk.config.ConfigSource;
|
18
|
+
import org.embulk.config.Task;
|
19
|
+
import org.embulk.config.TaskReport;
|
20
|
+
import org.embulk.config.TaskSource;
|
21
|
+
import org.embulk.output.ftp.SSLPlugins.SSLPluginConfig;
|
22
|
+
import org.embulk.spi.Buffer;
|
23
|
+
import org.embulk.spi.Exec;
|
24
|
+
import org.embulk.spi.FileOutputPlugin;
|
25
|
+
import org.embulk.spi.TransactionalFileOutput;
|
26
|
+
import org.embulk.spi.util.RetryExecutor.RetryGiveupException;
|
27
|
+
import org.embulk.spi.util.RetryExecutor.Retryable;
|
28
|
+
import org.slf4j.Logger;
|
29
|
+
import static org.embulk.spi.util.RetryExecutor.retryExecutor;
|
30
|
+
|
31
|
+
import java.io.BufferedInputStream;
|
32
|
+
import java.io.BufferedOutputStream;
|
33
|
+
import java.io.File;
|
34
|
+
import java.io.FileInputStream;
|
35
|
+
import java.io.FileNotFoundException;
|
36
|
+
import java.io.FileOutputStream;
|
37
|
+
import java.io.IOException;
|
38
|
+
import java.net.URISyntaxException;
|
39
|
+
import java.util.List;
|
40
|
+
|
41
|
+
public class FtpFileOutputPlugin implements FileOutputPlugin
|
42
|
+
{
|
43
|
+
public interface PluginTask extends Task, SSLPlugins.SSLPluginTask
|
44
|
+
{
|
45
|
+
@Config("host")
|
46
|
+
String getHost();
|
47
|
+
|
48
|
+
@Config("port")
|
49
|
+
@ConfigDefault("null")
|
50
|
+
Optional<Integer> getPort();
|
51
|
+
void setPort(Optional<Integer> port);
|
52
|
+
|
53
|
+
@Config("user")
|
54
|
+
@ConfigDefault("null")
|
55
|
+
Optional<String> getUser();
|
56
|
+
|
57
|
+
@Config("password")
|
58
|
+
@ConfigDefault("null")
|
59
|
+
Optional<String> getPassword();
|
60
|
+
|
61
|
+
@Config("passive_mode")
|
62
|
+
@ConfigDefault("true")
|
63
|
+
boolean getPassiveMode();
|
64
|
+
|
65
|
+
@Config("ascii_mode")
|
66
|
+
@ConfigDefault("false")
|
67
|
+
boolean getAsciiMode();
|
68
|
+
|
69
|
+
@Config("ssl")
|
70
|
+
@ConfigDefault("false")
|
71
|
+
boolean getSsl();
|
72
|
+
|
73
|
+
SSLPluginConfig getSSLConfig();
|
74
|
+
void setSSLConfig(SSLPluginConfig config);
|
75
|
+
|
76
|
+
@Config("path_prefix")
|
77
|
+
String getPathPrefix();
|
78
|
+
|
79
|
+
@Config("file_ext")
|
80
|
+
String getFileNameExtension();
|
81
|
+
|
82
|
+
@Config("sequence_format")
|
83
|
+
@ConfigDefault("\"%03d.%02d\"")
|
84
|
+
String getSequenceFormat();
|
85
|
+
|
86
|
+
@Config("max_connection_retry")
|
87
|
+
@ConfigDefault("10") // 10 times retry to connect FTP server if failed.
|
88
|
+
int getMaxConnectionRetry();
|
89
|
+
}
|
90
|
+
|
91
|
+
private static final Logger log = Exec.getLogger(FtpFileOutputPlugin.class);
|
92
|
+
private static final long TRANSFER_NOTICE_BYTES = 100 * 1024 * 1024;
|
93
|
+
|
94
|
+
@Override
|
95
|
+
public ConfigDiff transaction(ConfigSource config, int taskCount, FileOutputPlugin.Control control)
|
96
|
+
{
|
97
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
98
|
+
task.setSSLConfig(SSLPlugins.configure(task));
|
99
|
+
|
100
|
+
// try to check if plugin could connect to FTP server
|
101
|
+
FTPClient client = null;
|
102
|
+
try {
|
103
|
+
client = newFTPClient(log, task);
|
104
|
+
}
|
105
|
+
catch (Exception ex) {
|
106
|
+
throw new ConfigException(ex);
|
107
|
+
}
|
108
|
+
finally {
|
109
|
+
disconnectClient(client);
|
110
|
+
}
|
111
|
+
|
112
|
+
return resume(task.dump(), taskCount, control);
|
113
|
+
}
|
114
|
+
|
115
|
+
@Override
|
116
|
+
public ConfigDiff resume(TaskSource taskSource, int taskCount, FileOutputPlugin.Control control)
|
117
|
+
{
|
118
|
+
control.run(taskSource);
|
119
|
+
|
120
|
+
return Exec.newConfigDiff();
|
121
|
+
}
|
122
|
+
|
123
|
+
@Override
|
124
|
+
public void cleanup(TaskSource taskSource, int taskCount, List<TaskReport> successTaskReports)
|
125
|
+
{
|
126
|
+
}
|
127
|
+
|
128
|
+
@Override
|
129
|
+
public TransactionalFileOutput open(TaskSource taskSource, final int taskIndex)
|
130
|
+
{
|
131
|
+
final PluginTask task = taskSource.loadTask(PluginTask.class);
|
132
|
+
|
133
|
+
FTPClient client = newFTPClient(log, task);
|
134
|
+
return new FtpFileOutput(client, task, taskIndex);
|
135
|
+
}
|
136
|
+
|
137
|
+
public static class FtpFileOutput implements TransactionalFileOutput
|
138
|
+
{
|
139
|
+
private final FTPClient client;
|
140
|
+
private final String pathPrefix;
|
141
|
+
private final String sequenceFormat;
|
142
|
+
private final String pathSuffix;
|
143
|
+
private final int maxConnectionRetry;
|
144
|
+
private BufferedOutputStream output = null;
|
145
|
+
private int fileIndex;
|
146
|
+
private File file;
|
147
|
+
private String filePath;
|
148
|
+
private int taskIndex;
|
149
|
+
|
150
|
+
public FtpFileOutput(FTPClient client, PluginTask task, int taskIndex)
|
151
|
+
{
|
152
|
+
this.client = client;
|
153
|
+
this.taskIndex = taskIndex;
|
154
|
+
this.pathPrefix = task.getPathPrefix();
|
155
|
+
this.sequenceFormat = task.getSequenceFormat();
|
156
|
+
this.pathSuffix = task.getFileNameExtension();
|
157
|
+
this.maxConnectionRetry = task.getMaxConnectionRetry();
|
158
|
+
}
|
159
|
+
|
160
|
+
@Override
|
161
|
+
public void nextFile()
|
162
|
+
{
|
163
|
+
closeFile();
|
164
|
+
|
165
|
+
try {
|
166
|
+
String suffix = pathSuffix;
|
167
|
+
if (!suffix.startsWith(".")) {
|
168
|
+
suffix = "." + suffix;
|
169
|
+
}
|
170
|
+
filePath = pathPrefix + String.format(sequenceFormat, taskIndex, fileIndex) + suffix;
|
171
|
+
file = File.createTempFile(filePath, ".tmp");
|
172
|
+
log.info("Writing local file {}", file.getAbsolutePath());
|
173
|
+
output = new BufferedOutputStream(new FileOutputStream(file));
|
174
|
+
}
|
175
|
+
catch (IOException ex) {
|
176
|
+
throw Throwables.propagate(ex);
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
private void closeFile()
|
181
|
+
{
|
182
|
+
if (output != null) {
|
183
|
+
try {
|
184
|
+
output.close();
|
185
|
+
fileIndex++;
|
186
|
+
}
|
187
|
+
catch (IOException ex) {
|
188
|
+
throw Throwables.propagate(ex);
|
189
|
+
}
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
@Override
|
194
|
+
public void add(Buffer buffer)
|
195
|
+
{
|
196
|
+
try {
|
197
|
+
output.write(buffer.array(), buffer.offset(), buffer.limit());
|
198
|
+
}
|
199
|
+
catch (IOException ex) {
|
200
|
+
throw Throwables.propagate(ex);
|
201
|
+
}
|
202
|
+
finally {
|
203
|
+
buffer.release();
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
@Override
|
208
|
+
public void finish()
|
209
|
+
{
|
210
|
+
close();
|
211
|
+
uploadFile();
|
212
|
+
disconnectClient(client);
|
213
|
+
}
|
214
|
+
|
215
|
+
private Void uploadFile()
|
216
|
+
{
|
217
|
+
if (filePath != null) {
|
218
|
+
try {
|
219
|
+
return retryExecutor()
|
220
|
+
.withRetryLimit(maxConnectionRetry)
|
221
|
+
.withInitialRetryWait(500)
|
222
|
+
.withMaxRetryWait(30 * 1000)
|
223
|
+
.runInterruptible(new Retryable<Void>() {
|
224
|
+
@Override
|
225
|
+
public Void call() throws FTPIllegalReplyException, FTPException, FTPDataTransferException,
|
226
|
+
FTPAbortedException, IOException, RetryGiveupException
|
227
|
+
{
|
228
|
+
log.info("Upload start {} to {}", file.getAbsolutePath(), filePath);
|
229
|
+
client.upload(filePath, new BufferedInputStream(new FileInputStream(file)), 0L, 0L, new LoggingTransferListener(log, TRANSFER_NOTICE_BYTES));
|
230
|
+
log.info("Upload completed {} to {}", file.getAbsolutePath(), filePath);
|
231
|
+
if (!file.delete()) {
|
232
|
+
throw new ConfigException("Couldn't delete local file " + file.getAbsolutePath());
|
233
|
+
}
|
234
|
+
log.info("Delete local temporary file {}", file.getAbsolutePath());
|
235
|
+
return null;
|
236
|
+
}
|
237
|
+
|
238
|
+
@Override
|
239
|
+
public boolean isRetryableException(Exception exception)
|
240
|
+
{
|
241
|
+
return true;
|
242
|
+
}
|
243
|
+
|
244
|
+
@Override
|
245
|
+
public void onRetry(Exception exception, int retryCount, int retryLimit, int retryWait)
|
246
|
+
throws RetryGiveupException
|
247
|
+
{
|
248
|
+
if (exception instanceof FileNotFoundException || exception instanceof URISyntaxException || exception instanceof ConfigException) {
|
249
|
+
throw new RetryGiveupException(exception);
|
250
|
+
}
|
251
|
+
String message = String.format("FTP put request failed. Retrying %d/%d after %d seconds. Message: %s",
|
252
|
+
retryCount, retryLimit, retryWait / 1000, exception.getMessage());
|
253
|
+
if (retryCount % 3 == 0) {
|
254
|
+
log.warn(message, exception);
|
255
|
+
}
|
256
|
+
else {
|
257
|
+
log.warn(message);
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
@Override
|
262
|
+
public void onGiveup(Exception firstException, Exception lastException)
|
263
|
+
throws RetryGiveupException
|
264
|
+
{
|
265
|
+
}
|
266
|
+
});
|
267
|
+
}
|
268
|
+
catch (RetryGiveupException ex) {
|
269
|
+
throw Throwables.propagate(ex.getCause());
|
270
|
+
}
|
271
|
+
catch (InterruptedException ex) {
|
272
|
+
throw Throwables.propagate(ex);
|
273
|
+
}
|
274
|
+
}
|
275
|
+
return null;
|
276
|
+
}
|
277
|
+
|
278
|
+
@Override
|
279
|
+
public void close()
|
280
|
+
{
|
281
|
+
closeFile();
|
282
|
+
}
|
283
|
+
|
284
|
+
@Override
|
285
|
+
public void abort() {}
|
286
|
+
|
287
|
+
@Override
|
288
|
+
public TaskReport commit()
|
289
|
+
{
|
290
|
+
return Exec.newTaskReport();
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
private static FTPClient newFTPClient(Logger log, PluginTask task)
|
295
|
+
{
|
296
|
+
FTPClient client = new FTPClient();
|
297
|
+
try {
|
298
|
+
if (task.getSsl()) {
|
299
|
+
client.setSSLSocketFactory(SSLPlugins.newSSLSocketFactory(task.getSSLConfig(), task.getHost()));
|
300
|
+
client.setSecurity(FTPClient.SECURITY_FTPS);
|
301
|
+
if (!task.getPort().isPresent()) {
|
302
|
+
task.setPort(Optional.of(990));
|
303
|
+
}
|
304
|
+
}
|
305
|
+
else {
|
306
|
+
if (!task.getPort().isPresent()) {
|
307
|
+
task.setPort(Optional.of(21));
|
308
|
+
}
|
309
|
+
}
|
310
|
+
|
311
|
+
client.addCommunicationListener(new LoggingCommunicationListner(log));
|
312
|
+
|
313
|
+
// TODO configurable timeout parameters
|
314
|
+
client.setAutoNoopTimeout(3000);
|
315
|
+
|
316
|
+
FTPConnector con = client.getConnector();
|
317
|
+
con.setConnectionTimeout(30);
|
318
|
+
con.setReadTimeout(60);
|
319
|
+
con.setCloseTimeout(60);
|
320
|
+
|
321
|
+
// for commons-net client
|
322
|
+
//client.setControlKeepAliveTimeout
|
323
|
+
//client.setConnectTimeout
|
324
|
+
//client.setSoTimeout
|
325
|
+
//client.setDataTimeout
|
326
|
+
//client.setAutodetectUTF8
|
327
|
+
|
328
|
+
log.info("Connecting to {}", task.getHost());
|
329
|
+
if (task.getPort().isPresent()) {
|
330
|
+
client.connect(task.getHost(), task.getPort().get());
|
331
|
+
}
|
332
|
+
|
333
|
+
if (task.getUser().isPresent()) {
|
334
|
+
log.info("Logging in with user {}", task.getUser().get());
|
335
|
+
client.login(task.getUser().get(), task.getPassword().or(""));
|
336
|
+
}
|
337
|
+
|
338
|
+
log.info("Using passive mode");
|
339
|
+
client.setPassive(task.getPassiveMode());
|
340
|
+
|
341
|
+
if (task.getAsciiMode()) {
|
342
|
+
log.info("Using ASCII mode");
|
343
|
+
client.setType(FTPClient.TYPE_TEXTUAL);
|
344
|
+
}
|
345
|
+
else {
|
346
|
+
log.info("Using binary mode");
|
347
|
+
client.setType(FTPClient.TYPE_BINARY);
|
348
|
+
}
|
349
|
+
|
350
|
+
if (client.isCompressionSupported()) {
|
351
|
+
log.info("Using MODE Z compression");
|
352
|
+
client.setCompressionEnabled(true);
|
353
|
+
}
|
354
|
+
|
355
|
+
FTPClient connected = client;
|
356
|
+
client = null;
|
357
|
+
return connected;
|
358
|
+
}
|
359
|
+
catch (FTPException ex) {
|
360
|
+
log.info("FTP command failed: {}, {}", ex.getCode(), ex.getMessage());
|
361
|
+
throw Throwables.propagate(ex);
|
362
|
+
}
|
363
|
+
catch (FTPIllegalReplyException ex) {
|
364
|
+
log.info("FTP protocol error");
|
365
|
+
throw Throwables.propagate(ex);
|
366
|
+
}
|
367
|
+
catch (IOException ex) {
|
368
|
+
log.info("FTP network error: {}", ex);
|
369
|
+
throw Throwables.propagate(ex);
|
370
|
+
}
|
371
|
+
finally {
|
372
|
+
disconnectClient(client);
|
373
|
+
}
|
374
|
+
}
|
375
|
+
|
376
|
+
static void disconnectClient(FTPClient client)
|
377
|
+
{
|
378
|
+
if (client != null && client.isConnected()) {
|
379
|
+
try {
|
380
|
+
client.disconnect(false);
|
381
|
+
}
|
382
|
+
catch (FTPException | FTPIllegalReplyException | IOException ex) {
|
383
|
+
// do nothing
|
384
|
+
}
|
385
|
+
}
|
386
|
+
}
|
387
|
+
|
388
|
+
private static class LoggingCommunicationListner implements FTPCommunicationListener
|
389
|
+
{
|
390
|
+
private final Logger log;
|
391
|
+
|
392
|
+
public LoggingCommunicationListner(Logger log)
|
393
|
+
{
|
394
|
+
this.log = log;
|
395
|
+
}
|
396
|
+
|
397
|
+
public void received(String statement)
|
398
|
+
{
|
399
|
+
log.info("< " + statement);
|
400
|
+
}
|
401
|
+
|
402
|
+
public void sent(String statement)
|
403
|
+
{
|
404
|
+
if (statement.startsWith("PASS")) {
|
405
|
+
// don't show password
|
406
|
+
return;
|
407
|
+
}
|
408
|
+
log.info("> {}", statement);
|
409
|
+
}
|
410
|
+
}
|
411
|
+
|
412
|
+
private static class LoggingTransferListener implements FTPDataTransferListener
|
413
|
+
{
|
414
|
+
private final Logger log;
|
415
|
+
private final long transferNoticeBytes;
|
416
|
+
|
417
|
+
private long totalTransfer;
|
418
|
+
private long nextTransferNotice;
|
419
|
+
|
420
|
+
public LoggingTransferListener(Logger log, long transferNoticeBytes)
|
421
|
+
{
|
422
|
+
this.log = log;
|
423
|
+
this.transferNoticeBytes = transferNoticeBytes;
|
424
|
+
this.nextTransferNotice = transferNoticeBytes;
|
425
|
+
}
|
426
|
+
|
427
|
+
public void started()
|
428
|
+
{
|
429
|
+
log.info("Transfer started");
|
430
|
+
}
|
431
|
+
|
432
|
+
public void transferred(int length)
|
433
|
+
{
|
434
|
+
totalTransfer += length;
|
435
|
+
if (totalTransfer > nextTransferNotice) {
|
436
|
+
log.info("Transferred {} bytes", totalTransfer);
|
437
|
+
nextTransferNotice = ((totalTransfer / transferNoticeBytes) + 1) * transferNoticeBytes;
|
438
|
+
}
|
439
|
+
}
|
440
|
+
|
441
|
+
public void completed()
|
442
|
+
{
|
443
|
+
log.info("Transfer completed {} bytes", totalTransfer);
|
444
|
+
}
|
445
|
+
|
446
|
+
public void aborted()
|
447
|
+
{
|
448
|
+
log.info("Transfer aborted");
|
449
|
+
}
|
450
|
+
|
451
|
+
public void failed()
|
452
|
+
{
|
453
|
+
log.info("Transfer failed");
|
454
|
+
}
|
455
|
+
}
|
456
|
+
}
|
@@ -0,0 +1,260 @@
|
|
1
|
+
package org.embulk.output.ftp;
|
2
|
+
|
3
|
+
import com.fasterxml.jackson.annotation.JsonCreator;
|
4
|
+
import com.fasterxml.jackson.annotation.JsonIgnore;
|
5
|
+
import com.fasterxml.jackson.annotation.JsonProperty;
|
6
|
+
import com.google.common.base.Function;
|
7
|
+
import com.google.common.base.Optional;
|
8
|
+
import com.google.common.collect.ImmutableList;
|
9
|
+
import com.google.common.collect.Lists;
|
10
|
+
import org.embulk.config.Config;
|
11
|
+
import org.embulk.config.ConfigDefault;
|
12
|
+
import org.embulk.config.ConfigException;
|
13
|
+
|
14
|
+
import javax.net.ssl.SSLSocketFactory;
|
15
|
+
import javax.net.ssl.X509TrustManager;
|
16
|
+
|
17
|
+
import java.io.ByteArrayInputStream;
|
18
|
+
import java.io.FileReader;
|
19
|
+
import java.io.IOException;
|
20
|
+
import java.io.Reader;
|
21
|
+
import java.io.StringReader;
|
22
|
+
import java.security.GeneralSecurityException;
|
23
|
+
import java.security.KeyManagementException;
|
24
|
+
import java.security.cert.CertificateEncodingException;
|
25
|
+
import java.security.cert.CertificateException;
|
26
|
+
import java.security.cert.CertificateFactory;
|
27
|
+
import java.security.cert.X509Certificate;
|
28
|
+
import java.util.List;
|
29
|
+
|
30
|
+
public class SSLPlugins
|
31
|
+
{
|
32
|
+
// SSLPlugins is only for SSL clients. SSL server implementation is out ouf scope.
|
33
|
+
|
34
|
+
private SSLPlugins()
|
35
|
+
{
|
36
|
+
}
|
37
|
+
|
38
|
+
public interface SSLPluginTask
|
39
|
+
{
|
40
|
+
@Config("ssl_verify")
|
41
|
+
@ConfigDefault("null")
|
42
|
+
Optional<Boolean> getSslVerify();
|
43
|
+
|
44
|
+
@Config("ssl_verify_hostname")
|
45
|
+
@ConfigDefault("true")
|
46
|
+
boolean getSslVerifyHostname();
|
47
|
+
|
48
|
+
@Config("ssl_trusted_ca_cert_file")
|
49
|
+
@ConfigDefault("null")
|
50
|
+
Optional<String> getSslTrustedCaCertFile();
|
51
|
+
|
52
|
+
@Config("ssl_trusted_ca_cert_data")
|
53
|
+
@ConfigDefault("null")
|
54
|
+
Optional<String> getSslTrustedCaCertData();
|
55
|
+
}
|
56
|
+
|
57
|
+
private static enum VerifyMode
|
58
|
+
{
|
59
|
+
NO_VERIFY,
|
60
|
+
CERTIFICATES,
|
61
|
+
JVM_DEFAULT;
|
62
|
+
}
|
63
|
+
|
64
|
+
public static class SSLPluginConfig
|
65
|
+
{
|
66
|
+
static SSLPluginConfig noVerify = new SSLPluginConfig(VerifyMode.NO_VERIFY, false, ImmutableList.<byte[]>of());
|
67
|
+
|
68
|
+
private final VerifyMode verifyMode;
|
69
|
+
private final boolean verifyHostname;
|
70
|
+
private final List<X509Certificate> certificates;
|
71
|
+
|
72
|
+
@JsonCreator
|
73
|
+
private SSLPluginConfig(
|
74
|
+
@JsonProperty("verifyMode") VerifyMode verifyMode,
|
75
|
+
@JsonProperty("verifyHostname") boolean verifyHostname,
|
76
|
+
@JsonProperty("certificates") List<byte[]> certificates)
|
77
|
+
{
|
78
|
+
this.verifyMode = verifyMode;
|
79
|
+
this.verifyHostname = verifyHostname;
|
80
|
+
this.certificates = ImmutableList.copyOf(
|
81
|
+
Lists.transform(certificates, new Function<byte[], X509Certificate>() {
|
82
|
+
public X509Certificate apply(byte[] data)
|
83
|
+
{
|
84
|
+
try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
|
85
|
+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
86
|
+
return (X509Certificate) cf.generateCertificate(in);
|
87
|
+
}
|
88
|
+
catch (IOException | CertificateException ex) {
|
89
|
+
throw new RuntimeException(ex);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
})
|
93
|
+
);
|
94
|
+
}
|
95
|
+
|
96
|
+
SSLPluginConfig(List<X509Certificate> certificates, boolean verifyHostname)
|
97
|
+
{
|
98
|
+
this.verifyMode = VerifyMode.CERTIFICATES;
|
99
|
+
this.verifyHostname = verifyHostname;
|
100
|
+
this.certificates = certificates;
|
101
|
+
}
|
102
|
+
|
103
|
+
static SSLPluginConfig useJvmDefault(boolean verifyHostname)
|
104
|
+
{
|
105
|
+
return new SSLPluginConfig(VerifyMode.JVM_DEFAULT, verifyHostname, ImmutableList.<byte[]>of());
|
106
|
+
}
|
107
|
+
|
108
|
+
@JsonProperty("verifyMode")
|
109
|
+
private VerifyMode getVerifyMode()
|
110
|
+
{
|
111
|
+
return verifyMode;
|
112
|
+
}
|
113
|
+
|
114
|
+
@JsonProperty("verifyHostname")
|
115
|
+
private boolean getVerifyHostname()
|
116
|
+
{
|
117
|
+
return verifyHostname;
|
118
|
+
}
|
119
|
+
|
120
|
+
@JsonProperty("certificates")
|
121
|
+
private List<byte[]> getCertData()
|
122
|
+
{
|
123
|
+
return Lists.transform(certificates, new Function<X509Certificate, byte[]>() {
|
124
|
+
public byte[] apply(X509Certificate cert)
|
125
|
+
{
|
126
|
+
try {
|
127
|
+
return cert.getEncoded();
|
128
|
+
}
|
129
|
+
catch (CertificateEncodingException ex) {
|
130
|
+
throw new RuntimeException(ex);
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});
|
134
|
+
}
|
135
|
+
|
136
|
+
@JsonIgnore
|
137
|
+
public X509TrustManager[] newTrustManager()
|
138
|
+
{
|
139
|
+
try {
|
140
|
+
switch (verifyMode) {
|
141
|
+
case NO_VERIFY:
|
142
|
+
return new X509TrustManager[] { getNoVerifyTrustManager() };
|
143
|
+
case CERTIFICATES:
|
144
|
+
return TrustManagers.newTrustManager(certificates);
|
145
|
+
default: // JVM_DEFAULT
|
146
|
+
return TrustManagers.newDefaultJavaTrustManager();
|
147
|
+
}
|
148
|
+
}
|
149
|
+
catch (IOException | GeneralSecurityException ex) {
|
150
|
+
throw new RuntimeException(ex);
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
public static enum DefaultVerifyMode
|
156
|
+
{
|
157
|
+
VERIFY_BY_JVM_TRUSTED_CA_CERTS,
|
158
|
+
NO_VERIFY;
|
159
|
+
}
|
160
|
+
|
161
|
+
public static SSLPluginConfig configure(SSLPluginTask task)
|
162
|
+
{
|
163
|
+
return configure(task, DefaultVerifyMode.VERIFY_BY_JVM_TRUSTED_CA_CERTS);
|
164
|
+
}
|
165
|
+
|
166
|
+
public static SSLPluginConfig configure(SSLPluginTask task, DefaultVerifyMode defaultVerifyMode)
|
167
|
+
{
|
168
|
+
boolean verify = task.getSslVerify().or(defaultVerifyMode != DefaultVerifyMode.NO_VERIFY);
|
169
|
+
if (verify) {
|
170
|
+
Optional<List<X509Certificate>> certs = readTrustedCertificates(task);
|
171
|
+
if (certs.isPresent()) {
|
172
|
+
return new SSLPluginConfig(certs.get(), task.getSslVerifyHostname());
|
173
|
+
}
|
174
|
+
else {
|
175
|
+
return SSLPluginConfig.useJvmDefault(task.getSslVerifyHostname());
|
176
|
+
}
|
177
|
+
}
|
178
|
+
else {
|
179
|
+
return SSLPluginConfig.noVerify;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
private static Optional<List<X509Certificate>> readTrustedCertificates(SSLPluginTask task)
|
184
|
+
{
|
185
|
+
String optionName;
|
186
|
+
Reader reader;
|
187
|
+
if (task.getSslTrustedCaCertData().isPresent()) {
|
188
|
+
optionName = "ssl_trusted_ca_cert_data";
|
189
|
+
reader = new StringReader(task.getSslTrustedCaCertData().get());
|
190
|
+
}
|
191
|
+
else if (task.getSslTrustedCaCertFile().isPresent()) {
|
192
|
+
optionName = "ssl_trusted_ca_cert_file '" + task.getSslTrustedCaCertFile().get() + "'";
|
193
|
+
try {
|
194
|
+
reader = new FileReader(task.getSslTrustedCaCertFile().get());
|
195
|
+
}
|
196
|
+
catch (IOException ex) {
|
197
|
+
throw new ConfigException(String.format("Failed to open %s", optionName), ex);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
else {
|
201
|
+
return Optional.absent();
|
202
|
+
}
|
203
|
+
|
204
|
+
List<X509Certificate> certs;
|
205
|
+
try (Reader r = reader) {
|
206
|
+
certs = TrustManagers.readPemEncodedX509Certificates(r);
|
207
|
+
if (certs.isEmpty()) {
|
208
|
+
throw new ConfigException(String.format("%s does not include valid X.509 PEM certificates", optionName));
|
209
|
+
}
|
210
|
+
}
|
211
|
+
catch (CertificateException | IOException ex) {
|
212
|
+
throw new ConfigException(String.format("Failed to read %s", optionName), ex);
|
213
|
+
}
|
214
|
+
|
215
|
+
return Optional.of(certs);
|
216
|
+
}
|
217
|
+
|
218
|
+
public static SSLSocketFactory newSSLSocketFactory(SSLPluginConfig config, String hostname)
|
219
|
+
{
|
220
|
+
try {
|
221
|
+
return TrustManagers.newSSLSocketFactory(
|
222
|
+
null, // TODO sending client certificate is not implemented yet
|
223
|
+
config.newTrustManager(),
|
224
|
+
config.getVerifyHostname() ? hostname : null);
|
225
|
+
}
|
226
|
+
catch (KeyManagementException ex) {
|
227
|
+
throw new RuntimeException(ex);
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
private static class NoVerifyTrustManager implements X509TrustManager
|
232
|
+
{
|
233
|
+
static final NoVerifyTrustManager INSTANCE = new NoVerifyTrustManager();
|
234
|
+
|
235
|
+
private NoVerifyTrustManager()
|
236
|
+
{
|
237
|
+
}
|
238
|
+
|
239
|
+
@Override
|
240
|
+
public X509Certificate[] getAcceptedIssuers()
|
241
|
+
{
|
242
|
+
return null;
|
243
|
+
}
|
244
|
+
|
245
|
+
@Override
|
246
|
+
public void checkClientTrusted(X509Certificate[] certs, String authType)
|
247
|
+
{
|
248
|
+
}
|
249
|
+
|
250
|
+
@Override
|
251
|
+
public void checkServerTrusted(X509Certificate[] certs, String authType)
|
252
|
+
{
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
private static X509TrustManager getNoVerifyTrustManager()
|
257
|
+
{
|
258
|
+
return NoVerifyTrustManager.INSTANCE;
|
259
|
+
}
|
260
|
+
}
|