comparison tests/TLSTester.java @ 802:5d04490bc58b monetdbs

Add tests using monetdb-tlstester
author Joeri van Ruth <joeri.van.ruth@monetdbsolutions.com>
date Mon, 11 Dec 2023 13:45:12 +0100 (17 months ago)
parents
children 1671f2eb130b
comparison
equal deleted inserted replaced
801:88b3e8e89126 802:5d04490bc58b
1 import org.monetdb.mcl.net.Parameter;
2
3 import java.io.*;
4 import java.net.URL;
5 import java.net.URLConnection;
6 import java.nio.charset.StandardCharsets;
7 import java.nio.file.Files;
8 import java.sql.Connection;
9 import java.sql.DriverManager;
10 import java.sql.SQLException;
11 import java.util.HashMap;
12 import java.util.Properties;
13
14 public class TLSTester {
15 int verbose = 0;
16 String serverHost = null;
17 String altHost = null;
18 int serverPort = -1;
19 boolean enableTrusted = false;
20 File tempDir = null;
21 final HashMap<String, File> fileCache = new HashMap<>();
22
23 public TLSTester(String[] args) {
24 for (int i = 0; i < args.length; i++) {
25 String arg = args[i];
26 if (arg.equals("-v")) {
27 verbose = 1;
28 } else if (arg.equals("-a")) {
29 altHost = args[++i];
30 } else if (arg.equals("-t")) {
31 enableTrusted = true;
32 } else if (!arg.startsWith("-") && serverHost == null) {
33 int idx = arg.indexOf(':');
34 if (idx > 0) {
35 serverHost = arg.substring(0, idx);
36 try {
37 serverPort = Integer.parseInt(arg.substring(idx + 1));
38 if (serverPort > 0 && serverPort < 65536)
39 continue;
40 } catch (NumberFormatException ignored) {
41 }
42 }
43 // if we get here it wasn't very valid
44 throw new IllegalArgumentException("Invalid argument: " + arg);
45 } else {
46 throw new IllegalArgumentException("Unexpected argument: " + arg);
47 }
48 }
49 }
50
51 public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {
52 Class.forName("org.monetdb.jdbc.MonetDriver");
53 TLSTester main = new TLSTester(args);
54 main.run();
55 }
56
57 private HashMap<String,Integer> loadPortMap(String testName) throws IOException {
58 HashMap<String,Integer> portMap = new HashMap<>();
59 InputStream in = fetchData("/?test=" + testName);
60 BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
61 for (String line = br.readLine(); line != null; line = br.readLine()) {
62 int idx = line.indexOf(':');
63 String service = line.substring(0, idx);
64 int port;
65 try {
66 port = Integer.parseInt(line.substring(idx + 1));
67 } catch (NumberFormatException e) {
68 throw new RuntimeException("Invalid port map line: " + line);
69 }
70 portMap.put(service, port);
71 }
72 return portMap;
73 }
74
75 private File resource(String resource) throws IOException {
76 if (!fileCache.containsKey(resource))
77 fetchResource(resource);
78 return fileCache.get(resource);
79 }
80
81 private void fetchResource(String resource) throws IOException {
82 if (!resource.startsWith("/")) {
83 throw new IllegalArgumentException("Resource must start with slash: " + resource);
84 }
85 if (tempDir == null) {
86 tempDir = Files.createTempDirectory("tlstest").toFile();
87 tempDir.deleteOnExit();
88 }
89 File outPath = new File(tempDir, resource.substring(1));
90 try (InputStream in = fetchData(resource); FileOutputStream out = new FileOutputStream(outPath)) {
91 byte[] buffer = new byte[12];
92 while (true) {
93 int n = in.read(buffer);
94 if (n <= 0)
95 break;
96 out.write(buffer, 0, n);
97 }
98 }
99 fileCache.put(resource, outPath);
100 }
101
102 private byte[] fetchBytes(String resource) throws IOException {
103 ByteArrayOutputStream out = new ByteArrayOutputStream();
104 try (InputStream in = fetchData(resource)) {
105 byte[] buffer = new byte[22];
106 while (true) {
107 int nread = in.read(buffer);
108 if (nread <= 0)
109 break;
110 out.write(buffer, 0, nread);
111 }
112 return out.toByteArray();
113 }
114 }
115
116 private InputStream fetchData(String resource) throws IOException {
117 URL url = new URL("http://" + serverHost + ":" + serverPort + resource);
118 URLConnection conn = url.openConnection();
119 conn.connect();
120 return conn.getInputStream();
121 }
122
123 private void run() throws IOException, SQLException {
124 test_connect_plain();
125 test_connect_tls();
126 test_refuse_no_cert();
127 test_refuse_wrong_cert();
128 test_refuse_wrong_host();
129 test_refuse_tlsv12();
130 test_refuse_expired();
131 // test_connect_client_auth1();
132 // test_connect_client_auth2();
133 test_fail_tls_to_plain();
134 // test_fail_plain_to_tls();
135 // test_connect_server_name();
136 // test_connect_alpn_mapi9();
137 test_connect_trusted();
138 test_refuse_trusted_wrong_host();
139 }
140
141 private void test_connect_plain() throws IOException, SQLException {
142 attempt("connect_plain", "plain").with(Parameter.TLS, false).expectSuccess();
143 }
144
145 private void test_connect_tls() throws IOException, SQLException {
146 Attempt attempt = attempt("connect_tls", "server1");
147 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
148 }
149
150 private void test_refuse_no_cert() throws IOException, SQLException {
151 attempt("refuse_no_cert", "server1").expectFailure("PKIX path building failed");
152 }
153
154 private void test_refuse_wrong_cert() throws IOException, SQLException {
155 Attempt attempt = attempt("refuse_wrong_cert", "server1");
156 attempt.withFile(Parameter.CERT, "/ca2.crt").expectFailure("PKIX path building failed");
157 }
158
159 private void test_refuse_wrong_host() throws IOException, SQLException {
160 Attempt attempt = attempt("refuse_wrong_host", "server1").with(Parameter.HOST, altHost);
161 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("No subject alternative DNS name");
162 }
163
164 private void test_refuse_tlsv12() throws IOException, SQLException {
165 Attempt attempt = attempt("refuse_tlsv12", "tls12");
166 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("protocol_version");
167 }
168
169 private void test_refuse_expired() throws IOException, SQLException {
170 Attempt attempt = attempt("refuse_expired", "expiredcert");
171 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("PKIX path validation failed");
172 }
173
174 private void test_connect_client_auth1() throws IOException, SQLException {
175 attempt("connect_client_auth1", "clientauth")
176 .withFile(Parameter.CERT, "/ca1.crt")
177 .withFile(Parameter.CLIENTKEY, "/client2.keycrt")
178 .expectSuccess();
179 }
180
181 private void test_connect_client_auth2() throws IOException, SQLException {
182 attempt("connect_client_auth2", "clientauth")
183 .withFile(Parameter.CERT, "/ca1.crt")
184 .withFile(Parameter.CLIENTKEY, "/client2.key")
185 .withFile(Parameter.CLIENTCERT, "/client2.crt")
186 .expectSuccess();
187 }
188
189 private void test_fail_tls_to_plain() throws IOException, SQLException {
190 Attempt attempt = attempt("fail_tls_to_plain", "plain");
191 attempt.withFile(Parameter.CERT, "/ca1.crt").expectFailure("");
192
193 }
194
195 private void test_fail_plain_to_tls() throws IOException, SQLException {
196 attempt("fail_plain_to_tls", "server1").with(Parameter.TLS, false).expectFailure("asdf");
197 }
198
199 private void test_connect_server_name() throws IOException, SQLException {
200 Attempt attempt = attempt("connect_server_name", "sni");
201 attempt.withFile(Parameter.CERT, "/ca1.crt").expectSuccess();
202 }
203
204 private void test_connect_alpn_mapi9() throws IOException, SQLException {
205 attempt("connect_alpn_mapi9", "");
206 }
207
208 private void test_connect_trusted() throws IOException, SQLException {
209 attempt("connect_trusted", "alpn_mapi9")
210 .with(Parameter.HOST, "monetdb.ergates.nl")
211 .with(Parameter.PORT, 50000)
212 .expectSuccess();
213 }
214
215 private void test_refuse_trusted_wrong_host() throws IOException, SQLException {
216 attempt("connect_trusted", null)
217 .with(Parameter.HOST, "monetdbxyz.ergates.nl")
218 .with(Parameter.PORT, 50000)
219 .expectFailure("No subject alternative DNS name");
220 }
221
222 private Attempt attempt(String testName, String portName) throws IOException {
223 return new Attempt(testName, portName);
224 }
225
226 private class Attempt {
227 private final String testName;
228 private final Properties props = new Properties();
229 boolean disabled = false;
230
231 public Attempt(String testName, String portName) throws IOException {
232 HashMap<String, Integer> portMap = loadPortMap(testName);
233
234 this.testName = testName;
235 with(Parameter.TLS, true);
236 with(Parameter.HOST, serverHost);
237 with(Parameter.SO_TIMEOUT, 3000);
238 if (portName != null) {
239 Integer port = portMap.get(portName);
240 if (port != null) {
241 with(Parameter.PORT, port);
242 } else {
243 throw new RuntimeException("Unknown port name: " + portName);
244 }
245 }
246 }
247
248 private Attempt with(Parameter parm, String value) {
249 props.setProperty(parm.name, value);
250 return this;
251 }
252
253 private Attempt with(Parameter parm, int value) {
254 props.setProperty(parm.name, Integer.toString(value));
255 return this;
256 }
257
258 private Attempt with(Parameter parm, boolean value) {
259 props.setProperty(parm.name, value ? "true" : "false");
260 return this;
261 }
262
263 private Attempt withFile(Parameter parm, String certResource) throws IOException {
264 File certFile = resource(certResource);
265 String path = certFile.getPath();
266 with(parm, path);
267 return this;
268 }
269
270 public void expectSuccess() throws SQLException {
271 if (disabled)
272 return;
273 try {
274 Connection conn = DriverManager.getConnection("jdbc:monetdb:", props);
275 conn.close();
276 } catch (SQLException e) {
277 if (e.getMessage().startsWith("Sorry, this is not a real MonetDB instance")) {
278 // it looks like a failure but this is actually our success scenario
279 // because this is what the TLS Tester does when the connection succeeds.
280 return;
281 }
282 // other exceptions ARE errors and should be reported.
283 throw e;
284 }
285 }
286
287 public void expectFailure(String... expectedMessages) throws SQLException {
288 if (disabled)
289 return;
290 try {
291 expectSuccess();
292 throw new RuntimeException("Expected test " + testName + " to throw an exception but it didn't");
293 } catch (SQLException e) {
294 for (String expected : expectedMessages)
295 if (e.getMessage().contains(expected))
296 return;
297 String message = "Test " + testName + " threw the wrong exception: " + e.getMessage() + '\n' + "Expected:\n <" + String.join(">\n <", expectedMessages) + ">";
298 throw new RuntimeException(message);
299
300 }
301 }
302
303 }
304 }