1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.server; |
7 | |
8 | import java.io.IOException; |
9 | import java.net.ServerSocket; |
10 | import java.net.Socket; |
11 | import java.net.UnknownHostException; |
12 | import java.sql.Connection; |
13 | import java.sql.DriverManager; |
14 | import java.sql.PreparedStatement; |
15 | import java.sql.SQLException; |
16 | import java.sql.Statement; |
17 | import java.util.Collections; |
18 | import java.util.HashMap; |
19 | import java.util.HashSet; |
20 | import java.util.Map; |
21 | import java.util.Properties; |
22 | import java.util.Set; |
23 | import org.h2.Driver; |
24 | import org.h2.api.ErrorCode; |
25 | import org.h2.engine.Constants; |
26 | import org.h2.message.DbException; |
27 | import org.h2.util.JdbcUtils; |
28 | import org.h2.util.NetUtils; |
29 | import org.h2.util.New; |
30 | import org.h2.util.StringUtils; |
31 | import org.h2.util.Tool; |
32 | |
33 | /** |
34 | * The TCP server implements the native H2 database server protocol. |
35 | * It supports multiple client connections to multiple databases |
36 | * (many to many). The same database may be opened by multiple clients. |
37 | * Also supported is the mixed mode: opening databases in embedded mode, |
38 | * and at the same time start a TCP server to allow clients to connect to |
39 | * the same database over the network. |
40 | */ |
41 | public class TcpServer implements Service { |
42 | |
43 | private static final int SHUTDOWN_NORMAL = 0; |
44 | private static final int SHUTDOWN_FORCE = 1; |
45 | |
46 | /** |
47 | * The name of the in-memory management database used by the TCP server |
48 | * to keep the active sessions. |
49 | */ |
50 | private static final String MANAGEMENT_DB_PREFIX = "management_db_"; |
51 | |
52 | private static final Map<Integer, TcpServer> SERVERS = |
53 | Collections.synchronizedMap(new HashMap<Integer, TcpServer>()); |
54 | |
55 | private int port; |
56 | private boolean portIsSet; |
57 | private boolean trace; |
58 | private boolean ssl; |
59 | private boolean stop; |
60 | private ShutdownHandler shutdownHandler; |
61 | private ServerSocket serverSocket; |
62 | private final Set<TcpServerThread> running = |
63 | Collections.synchronizedSet(new HashSet<TcpServerThread>()); |
64 | private String baseDir; |
65 | private boolean allowOthers; |
66 | private boolean isDaemon; |
67 | private boolean ifExists; |
68 | private Connection managementDb; |
69 | private PreparedStatement managementDbAdd; |
70 | private PreparedStatement managementDbRemove; |
71 | private String managementPassword = ""; |
72 | private Thread listenerThread; |
73 | private int nextThreadId; |
74 | private String key, keyDatabase; |
75 | |
76 | /** |
77 | * Get the database name of the management database. |
78 | * The management database contains a table with active sessions (SESSIONS). |
79 | * |
80 | * @param port the TCP server port |
81 | * @return the database name (usually starting with mem:) |
82 | */ |
83 | public static String getManagementDbName(int port) { |
84 | return "mem:" + MANAGEMENT_DB_PREFIX + port; |
85 | } |
86 | |
87 | private void initManagementDb() throws SQLException { |
88 | Properties prop = new Properties(); |
89 | prop.setProperty("user", ""); |
90 | prop.setProperty("password", managementPassword); |
91 | // avoid using the driver manager |
92 | Connection conn = Driver.load().connect("jdbc:h2:" + |
93 | getManagementDbName(port), prop); |
94 | managementDb = conn; |
95 | Statement stat = null; |
96 | try { |
97 | stat = conn.createStatement(); |
98 | stat.execute("CREATE ALIAS IF NOT EXISTS STOP_SERVER FOR \"" + |
99 | TcpServer.class.getName() + ".stopServer\""); |
100 | stat.execute("CREATE TABLE IF NOT EXISTS SESSIONS" + |
101 | "(ID INT PRIMARY KEY, URL VARCHAR, USER VARCHAR, " + |
102 | "CONNECTED TIMESTAMP)"); |
103 | managementDbAdd = conn.prepareStatement( |
104 | "INSERT INTO SESSIONS VALUES(?, ?, ?, NOW())"); |
105 | managementDbRemove = conn.prepareStatement( |
106 | "DELETE FROM SESSIONS WHERE ID=?"); |
107 | } finally { |
108 | JdbcUtils.closeSilently(stat); |
109 | } |
110 | SERVERS.put(port, this); |
111 | } |
112 | |
113 | /** |
114 | * Shut down this server. |
115 | */ |
116 | void shutdown() { |
117 | if (shutdownHandler != null) { |
118 | shutdownHandler.shutdown(); |
119 | } |
120 | } |
121 | |
122 | public void setShutdownHandler(ShutdownHandler shutdownHandler) { |
123 | this.shutdownHandler = shutdownHandler; |
124 | } |
125 | |
126 | /** |
127 | * Add a connection to the management database. |
128 | * |
129 | * @param id the connection id |
130 | * @param url the database URL |
131 | * @param user the user name |
132 | */ |
133 | synchronized void addConnection(int id, String url, String user) { |
134 | try { |
135 | managementDbAdd.setInt(1, id); |
136 | managementDbAdd.setString(2, url); |
137 | managementDbAdd.setString(3, user); |
138 | managementDbAdd.execute(); |
139 | } catch (SQLException e) { |
140 | DbException.traceThrowable(e); |
141 | } |
142 | } |
143 | |
144 | /** |
145 | * Remove a connection from the management database. |
146 | * |
147 | * @param id the connection id |
148 | */ |
149 | synchronized void removeConnection(int id) { |
150 | try { |
151 | managementDbRemove.setInt(1, id); |
152 | managementDbRemove.execute(); |
153 | } catch (SQLException e) { |
154 | DbException.traceThrowable(e); |
155 | } |
156 | } |
157 | |
158 | private synchronized void stopManagementDb() { |
159 | if (managementDb != null) { |
160 | try { |
161 | managementDb.close(); |
162 | } catch (SQLException e) { |
163 | DbException.traceThrowable(e); |
164 | } |
165 | managementDb = null; |
166 | } |
167 | } |
168 | |
169 | @Override |
170 | public void init(String... args) { |
171 | port = Constants.DEFAULT_TCP_PORT; |
172 | for (int i = 0; args != null && i < args.length; i++) { |
173 | String a = args[i]; |
174 | if (Tool.isOption(a, "-trace")) { |
175 | trace = true; |
176 | } else if (Tool.isOption(a, "-tcpSSL")) { |
177 | ssl = true; |
178 | } else if (Tool.isOption(a, "-tcpPort")) { |
179 | port = Integer.decode(args[++i]); |
180 | portIsSet = true; |
181 | } else if (Tool.isOption(a, "-tcpPassword")) { |
182 | managementPassword = args[++i]; |
183 | } else if (Tool.isOption(a, "-baseDir")) { |
184 | baseDir = args[++i]; |
185 | } else if (Tool.isOption(a, "-key")) { |
186 | key = args[++i]; |
187 | keyDatabase = args[++i]; |
188 | } else if (Tool.isOption(a, "-tcpAllowOthers")) { |
189 | allowOthers = true; |
190 | } else if (Tool.isOption(a, "-tcpDaemon")) { |
191 | isDaemon = true; |
192 | } else if (Tool.isOption(a, "-ifExists")) { |
193 | ifExists = true; |
194 | } |
195 | } |
196 | org.h2.Driver.load(); |
197 | } |
198 | |
199 | @Override |
200 | public String getURL() { |
201 | return (ssl ? "ssl" : "tcp") + "://" + NetUtils.getLocalAddress() + ":" + port; |
202 | } |
203 | |
204 | @Override |
205 | public int getPort() { |
206 | return port; |
207 | } |
208 | |
209 | /** |
210 | * Check if this socket may connect to this server. Remote connections are |
211 | * not allowed if the flag allowOthers is set. |
212 | * |
213 | * @param socket the socket |
214 | * @return true if this client may connect |
215 | */ |
216 | boolean allow(Socket socket) { |
217 | if (allowOthers) { |
218 | return true; |
219 | } |
220 | try { |
221 | return NetUtils.isLocalAddress(socket); |
222 | } catch (UnknownHostException e) { |
223 | traceError(e); |
224 | return false; |
225 | } |
226 | } |
227 | |
228 | @Override |
229 | public synchronized void start() throws SQLException { |
230 | stop = false; |
231 | try { |
232 | serverSocket = NetUtils.createServerSocket(port, ssl); |
233 | } catch (DbException e) { |
234 | if (!portIsSet) { |
235 | serverSocket = NetUtils.createServerSocket(0, ssl); |
236 | } else { |
237 | throw e; |
238 | } |
239 | } |
240 | port = serverSocket.getLocalPort(); |
241 | initManagementDb(); |
242 | } |
243 | |
244 | @Override |
245 | public void listen() { |
246 | listenerThread = Thread.currentThread(); |
247 | String threadName = listenerThread.getName(); |
248 | try { |
249 | while (!stop) { |
250 | Socket s = serverSocket.accept(); |
251 | TcpServerThread c = new TcpServerThread(s, this, nextThreadId++); |
252 | running.add(c); |
253 | Thread thread = new Thread(c, threadName + " thread"); |
254 | thread.setDaemon(isDaemon); |
255 | c.setThread(thread); |
256 | thread.start(); |
257 | } |
258 | serverSocket = NetUtils.closeSilently(serverSocket); |
259 | } catch (Exception e) { |
260 | if (!stop) { |
261 | DbException.traceThrowable(e); |
262 | } |
263 | } |
264 | stopManagementDb(); |
265 | } |
266 | |
267 | @Override |
268 | public synchronized boolean isRunning(boolean traceError) { |
269 | if (serverSocket == null) { |
270 | return false; |
271 | } |
272 | try { |
273 | Socket s = NetUtils.createLoopbackSocket(port, ssl); |
274 | s.close(); |
275 | return true; |
276 | } catch (Exception e) { |
277 | if (traceError) { |
278 | traceError(e); |
279 | } |
280 | return false; |
281 | } |
282 | } |
283 | |
284 | @Override |
285 | public void stop() { |
286 | // TODO server: share code between web and tcp servers |
287 | // need to remove the server first, otherwise the connection is broken |
288 | // while the server is still registered in this map |
289 | SERVERS.remove(port); |
290 | if (!stop) { |
291 | stopManagementDb(); |
292 | stop = true; |
293 | if (serverSocket != null) { |
294 | try { |
295 | serverSocket.close(); |
296 | } catch (IOException e) { |
297 | DbException.traceThrowable(e); |
298 | } catch (NullPointerException e) { |
299 | // ignore |
300 | } |
301 | serverSocket = null; |
302 | } |
303 | if (listenerThread != null) { |
304 | try { |
305 | listenerThread.join(1000); |
306 | } catch (InterruptedException e) { |
307 | DbException.traceThrowable(e); |
308 | } |
309 | } |
310 | } |
311 | // TODO server: using a boolean 'now' argument? a timeout? |
312 | for (TcpServerThread c : New.arrayList(running)) { |
313 | if (c != null) { |
314 | c.close(); |
315 | try { |
316 | c.getThread().join(100); |
317 | } catch (Exception e) { |
318 | DbException.traceThrowable(e); |
319 | } |
320 | } |
321 | } |
322 | } |
323 | |
324 | /** |
325 | * Stop a running server. This method is called via reflection from the |
326 | * STOP_SERVER function. |
327 | * |
328 | * @param port the port where the server runs, or 0 for all running servers |
329 | * @param password the password (or null) |
330 | * @param shutdownMode the shutdown mode, SHUTDOWN_NORMAL or SHUTDOWN_FORCE. |
331 | */ |
332 | public static void stopServer(int port, String password, int shutdownMode) { |
333 | if (port == 0) { |
334 | for (int p : SERVERS.keySet().toArray(new Integer[0])) { |
335 | if (p != 0) { |
336 | stopServer(p, password, shutdownMode); |
337 | } |
338 | } |
339 | return; |
340 | } |
341 | TcpServer server = SERVERS.get(port); |
342 | if (server == null) { |
343 | return; |
344 | } |
345 | if (!server.managementPassword.equals(password)) { |
346 | return; |
347 | } |
348 | if (shutdownMode == SHUTDOWN_NORMAL) { |
349 | server.stopManagementDb(); |
350 | server.stop = true; |
351 | try { |
352 | Socket s = NetUtils.createLoopbackSocket(port, false); |
353 | s.close(); |
354 | } catch (Exception e) { |
355 | // try to connect - so that accept returns |
356 | } |
357 | } else if (shutdownMode == SHUTDOWN_FORCE) { |
358 | server.stop(); |
359 | } |
360 | server.shutdown(); |
361 | } |
362 | |
363 | /** |
364 | * Remove a thread from the list. |
365 | * |
366 | * @param t the thread to remove |
367 | */ |
368 | void remove(TcpServerThread t) { |
369 | running.remove(t); |
370 | } |
371 | |
372 | /** |
373 | * Get the configured base directory. |
374 | * |
375 | * @return the base directory |
376 | */ |
377 | String getBaseDir() { |
378 | return baseDir; |
379 | } |
380 | |
381 | /** |
382 | * Print a message if the trace flag is enabled. |
383 | * |
384 | * @param s the message |
385 | */ |
386 | void trace(String s) { |
387 | if (trace) { |
388 | System.out.println(s); |
389 | } |
390 | } |
391 | /** |
392 | * Print a stack trace if the trace flag is enabled. |
393 | * |
394 | * @param e the exception |
395 | */ |
396 | void traceError(Throwable e) { |
397 | if (trace) { |
398 | e.printStackTrace(); |
399 | } |
400 | } |
401 | |
402 | @Override |
403 | public boolean getAllowOthers() { |
404 | return allowOthers; |
405 | } |
406 | |
407 | @Override |
408 | public String getType() { |
409 | return "TCP"; |
410 | } |
411 | |
412 | @Override |
413 | public String getName() { |
414 | return "H2 TCP Server"; |
415 | } |
416 | |
417 | boolean getIfExists() { |
418 | return ifExists; |
419 | } |
420 | |
421 | /** |
422 | * Stop the TCP server with the given URL. |
423 | * |
424 | * @param url the database URL |
425 | * @param password the password |
426 | * @param force if the server should be stopped immediately |
427 | * @param all whether all TCP servers that are running in the JVM should be |
428 | * stopped |
429 | */ |
430 | public static synchronized void shutdown(String url, String password, |
431 | boolean force, boolean all) throws SQLException { |
432 | try { |
433 | int port = Constants.DEFAULT_TCP_PORT; |
434 | int idx = url.lastIndexOf(':'); |
435 | if (idx >= 0) { |
436 | String p = url.substring(idx + 1); |
437 | if (StringUtils.isNumber(p)) { |
438 | port = Integer.decode(p); |
439 | } |
440 | } |
441 | String db = getManagementDbName(port); |
442 | try { |
443 | org.h2.Driver.load(); |
444 | } catch (Throwable e) { |
445 | throw DbException.convert(e); |
446 | } |
447 | for (int i = 0; i < 2; i++) { |
448 | Connection conn = null; |
449 | PreparedStatement prep = null; |
450 | try { |
451 | conn = DriverManager.getConnection("jdbc:h2:" + url + "/" + db, "", password); |
452 | prep = conn.prepareStatement("CALL STOP_SERVER(?, ?, ?)"); |
453 | prep.setInt(1, all ? 0 : port); |
454 | prep.setString(2, password); |
455 | prep.setInt(3, force ? SHUTDOWN_FORCE : SHUTDOWN_NORMAL); |
456 | try { |
457 | prep.execute(); |
458 | } catch (SQLException e) { |
459 | if (force) { |
460 | // ignore |
461 | } else { |
462 | if (e.getErrorCode() != ErrorCode.CONNECTION_BROKEN_1) { |
463 | throw e; |
464 | } |
465 | } |
466 | } |
467 | break; |
468 | } catch (SQLException e) { |
469 | if (i == 1) { |
470 | throw e; |
471 | } |
472 | } finally { |
473 | JdbcUtils.closeSilently(prep); |
474 | JdbcUtils.closeSilently(conn); |
475 | } |
476 | } |
477 | } catch (Exception e) { |
478 | throw DbException.toSQLException(e); |
479 | } |
480 | } |
481 | |
482 | /** |
483 | * Cancel a running statement. |
484 | * |
485 | * @param sessionId the session id |
486 | * @param statementId the statement id |
487 | */ |
488 | void cancelStatement(String sessionId, int statementId) { |
489 | for (TcpServerThread c : New.arrayList(running)) { |
490 | if (c != null) { |
491 | c.cancelStatement(sessionId, statementId); |
492 | } |
493 | } |
494 | } |
495 | |
496 | /** |
497 | * If no key is set, return the original database name. If a key is set, |
498 | * check if the key matches. If yes, return the correct database name. If |
499 | * not, throw an exception. |
500 | * |
501 | * @param db the key to test (or database name if no key is used) |
502 | * @return the database name |
503 | * @throws DbException if a key is set but doesn't match |
504 | */ |
505 | public String checkKeyAndGetDatabaseName(String db) { |
506 | if (key == null) { |
507 | return db; |
508 | } |
509 | if (key.equals(db)) { |
510 | return keyDatabase; |
511 | } |
512 | throw DbException.get(ErrorCode.WRONG_USER_OR_PASSWORD); |
513 | } |
514 | |
515 | @Override |
516 | public boolean isDaemon() { |
517 | return isDaemon; |
518 | } |
519 | |
520 | } |