Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
WebServer |
|
| 3.0526315789473686;3,053 |
1 | /* |
|
2 | * Copyright 1999,2005 The Apache Software Foundation. |
|
3 | * |
|
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
|
5 | * you may not use this file except in compliance with the License. |
|
6 | * You may obtain a copy of the License at |
|
7 | * |
|
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
|
9 | * |
|
10 | * Unless required by applicable law or agreed to in writing, software |
|
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13 | * See the License for the specific language governing permissions and |
|
14 | * limitations under the License. |
|
15 | */ |
|
16 | package org.apache.xmlrpc.webserver; |
|
17 | ||
18 | import java.io.IOException; |
|
19 | import java.io.InterruptedIOException; |
|
20 | import java.net.BindException; |
|
21 | import java.net.InetAddress; |
|
22 | import java.net.ServerSocket; |
|
23 | import java.net.Socket; |
|
24 | import java.net.SocketException; |
|
25 | import java.util.ArrayList; |
|
26 | import java.util.List; |
|
27 | import java.util.StringTokenizer; |
|
28 | ||
29 | import org.apache.commons.logging.Log; |
|
30 | import org.apache.commons.logging.LogFactory; |
|
31 | import org.apache.xmlrpc.server.XmlRpcStreamServer; |
|
32 | import org.apache.xmlrpc.util.ThreadPool; |
|
33 | ||
34 | ||
35 | /** A minimal web server that exclusively handles XML-RPC requests. |
|
36 | */ |
|
37 | 1 | public class WebServer implements Runnable { |
38 | 1 | private static final Log log = LogFactory.getLog(WebServer.class); |
39 | ||
40 | private class AddressMatcher { |
|
41 | private final int pattern[]; |
|
42 | ||
43 | 0 | AddressMatcher(String address) { |
44 | try { |
|
45 | 0 | pattern = new int[4]; |
46 | 0 | StringTokenizer st = new StringTokenizer(address, "."); |
47 | 0 | if (st.countTokens() != 4) { |
48 | 0 | throw new IllegalArgumentException(); |
49 | } |
|
50 | 0 | for (int i = 0; i < 4; i++) { |
51 | 0 | String next = st.nextToken(); |
52 | 0 | if ("*".equals(next)) { |
53 | 0 | pattern[i] = 256; |
54 | } else { |
|
55 | 0 | pattern[i] = Integer.parseInt(next); |
56 | } |
|
57 | } |
|
58 | 0 | } catch (Exception e) { |
59 | 0 | throw new IllegalArgumentException("\"" + address |
60 | 0 | + "\" does not represent a valid IP address"); |
61 | } |
|
62 | 0 | } |
63 | ||
64 | boolean matches(byte[] pAddress) { |
|
65 | 0 | for (int i = 0; i < 4; i++) { |
66 | 0 | if (pattern[i] > 255) { |
67 | 0 | continue; // Wildcard |
68 | } |
|
69 | 0 | if (pattern[i] != pAddress[i]) { |
70 | 0 | return false; |
71 | } |
|
72 | } |
|
73 | 0 | return true; |
74 | } |
|
75 | } |
|
76 | ||
77 | protected ServerSocket serverSocket; |
|
78 | private Thread listener; |
|
79 | private ThreadPool pool; |
|
80 | 154 | protected final List accept = new ArrayList(); |
81 | 154 | protected final List deny = new ArrayList(); |
82 | 154 | protected final XmlRpcStreamServer server = newXmlRpcStreamServer(); |
83 | ||
84 | protected XmlRpcStreamServer newXmlRpcStreamServer(){ |
|
85 | 154 | return new ConnectionServer(); |
86 | } |
|
87 | ||
88 | // Inputs to setupServerSocket() |
|
89 | private InetAddress address; |
|
90 | private int port; |
|
91 | ||
92 | private boolean paranoid; |
|
93 | ||
94 | static final String HTTP_11 = "HTTP/1.1"; |
|
95 | /** Creates a web server at the specified port number. |
|
96 | * @param pPort Port number; 0 for a random port, choosen by the |
|
97 | * operating system. |
|
98 | */ |
|
99 | public WebServer(int pPort) { |
|
100 | 110 | this(pPort, null); |
101 | 110 | } |
102 | ||
103 | /** Creates a web server at the specified port number and IP address. |
|
104 | * @param pPort Port number; 0 for a random port, choosen by the |
|
105 | * operating system. |
|
106 | * @param pAddr Local IP address; null for all available IP addresses. |
|
107 | */ |
|
108 | 154 | public WebServer(int pPort, InetAddress pAddr) { |
109 | 154 | address = pAddr; |
110 | 154 | port = pPort; |
111 | 154 | } |
112 | ||
113 | /** |
|
114 | * Factory method to manufacture the server socket. Useful as a |
|
115 | * hook method for subclasses to override when they desire |
|
116 | * different flavor of socket (i.e. a <code>SSLServerSocket</code>). |
|
117 | * |
|
118 | * @param pPort Port number; 0 for a random port, choosen by the operating |
|
119 | * system. |
|
120 | * @param backlog |
|
121 | * @param addr If <code>null</code>, binds to |
|
122 | * <code>INADDR_ANY</code>, meaning that all network interfaces on |
|
123 | * a multi-homed host will be listening. |
|
124 | * @exception IOException Error creating listener socket. |
|
125 | */ |
|
126 | protected ServerSocket createServerSocket(int pPort, int backlog, InetAddress addr) |
|
127 | throws IOException { |
|
128 | 154 | return new ServerSocket(pPort, backlog, addr); |
129 | } |
|
130 | ||
131 | /** |
|
132 | * Initializes this server's listener socket with the specified |
|
133 | * attributes, assuring that a socket timeout has been set. The |
|
134 | * {@link #createServerSocket(int, int, InetAddress)} method can |
|
135 | * be overridden to change the flavor of socket used. |
|
136 | * |
|
137 | * @see #createServerSocket(int, int, InetAddress) |
|
138 | */ |
|
139 | private synchronized void setupServerSocket(int backlog) throws IOException { |
|
140 | // Since we can't reliably set SO_REUSEADDR until JDK 1.4 is |
|
141 | // the standard, try to (re-)open the server socket several |
|
142 | // times. Some OSes (Linux and Solaris, for example), hold on |
|
143 | // to listener sockets for a brief period of time for security |
|
144 | // reasons before relinquishing their hold. |
|
145 | 154 | for (int i = 1; ; i++) { |
146 | try { |
|
147 | 154 | serverSocket = createServerSocket(port, backlog, address); |
148 | // A socket timeout must be set. |
|
149 | 154 | if (serverSocket.getSoTimeout() <= 0) { |
150 | 154 | serverSocket.setSoTimeout(4096); |
151 | } |
|
152 | 154 | return; |
153 | 0 | } catch (BindException e) { |
154 | 0 | if (i == 10) { |
155 | 0 | throw e; |
156 | } else { |
|
157 | 0 | long waitUntil = System.currentTimeMillis(); |
158 | 0 | for (;;) { |
159 | 0 | long l = waitUntil - System.currentTimeMillis(); |
160 | 0 | if (l > 0) { |
161 | try { |
|
162 | 0 | Thread.sleep(l); |
163 | 0 | } catch (InterruptedException ex) { |
164 | } |
|
165 | } |
|
166 | } |
|
167 | } |
|
168 | } |
|
169 | } |
|
170 | } |
|
171 | ||
172 | /** |
|
173 | * Spawns a new thread which binds this server to the port it's |
|
174 | * configured to accept connections on. |
|
175 | * |
|
176 | * @see #run() |
|
177 | * @throws IOException Binding the server socket failed. |
|
178 | */ |
|
179 | public void start() throws IOException { |
|
180 | 154 | setupServerSocket(50); |
181 | ||
182 | // The listener reference is released upon shutdown(). |
|
183 | 154 | if (listener == null) { |
184 | 154 | listener = new Thread(this, "XML-RPC Weblistener"); |
185 | // Not marked as daemon thread since run directly via main(). |
|
186 | 154 | listener.start(); |
187 | } |
|
188 | 154 | } |
189 | ||
190 | /** |
|
191 | * Switch client filtering on/off. |
|
192 | * @param pParanoid True to enable filtering, false otherwise. |
|
193 | * @see #acceptClient(java.lang.String) |
|
194 | * @see #denyClient(java.lang.String) |
|
195 | */ |
|
196 | public void setParanoid(boolean pParanoid) { |
|
197 | 0 | paranoid = pParanoid; |
198 | 0 | } |
199 | ||
200 | /** Add an IP address to the list of accepted clients. The parameter can |
|
201 | * contain '*' as wildcard character, e.g. "192.168.*.*". You must call |
|
202 | * setParanoid(true) in order for this to have any effect. |
|
203 | * @param pAddress The IP address being enabled. |
|
204 | * @see #denyClient(java.lang.String) |
|
205 | * @see #setParanoid(boolean) |
|
206 | * @throws IllegalArgumentException Parsing the address failed. |
|
207 | */ |
|
208 | public void acceptClient(String pAddress) { |
|
209 | 0 | accept.add(new AddressMatcher(pAddress)); |
210 | 0 | } |
211 | ||
212 | /** |
|
213 | * Add an IP address to the list of denied clients. The parameter can |
|
214 | * contain '*' as wildcard character, e.g. "192.168.*.*". You must call |
|
215 | * setParanoid(true) in order for this to have any effect. |
|
216 | * @param pAddress The IP address being disabled. |
|
217 | * @see #acceptClient(java.lang.String) |
|
218 | * @see #setParanoid(boolean) |
|
219 | * @throws IllegalArgumentException Parsing the address failed. |
|
220 | */ |
|
221 | public void denyClient(String pAddress) { |
|
222 | 0 | deny.add(new AddressMatcher(pAddress)); |
223 | 0 | } |
224 | ||
225 | /** |
|
226 | * Checks incoming connections to see if they should be allowed. |
|
227 | * If not in paranoid mode, always returns true. |
|
228 | * |
|
229 | * @param s The socket to inspect. |
|
230 | * @return Whether the connection should be allowed. |
|
231 | */ |
|
232 | protected boolean allowConnection(Socket s) { |
|
233 | 245 | if (!paranoid) { |
234 | 245 | return true; |
235 | } |
|
236 | ||
237 | 0 | int l = deny.size(); |
238 | 0 | byte addr[] = s.getInetAddress().getAddress(); |
239 | 0 | for (int i = 0; i < l; i++) { |
240 | 0 | AddressMatcher match = (AddressMatcher) deny.get(i); |
241 | 0 | if (match.matches(addr)) |
242 | { |
|
243 | 0 | return false; |
244 | } |
|
245 | } |
|
246 | 0 | l = accept.size(); |
247 | 0 | for (int i = 0; i < l; i++) { |
248 | 0 | AddressMatcher match = (AddressMatcher) accept.get(i); |
249 | 0 | if (match.matches(addr)) { |
250 | 0 | return true; |
251 | } |
|
252 | } |
|
253 | 0 | return false; |
254 | } |
|
255 | ||
256 | protected ThreadPool.Task newTask(WebServer pServer, XmlRpcStreamServer pXmlRpcServer, |
|
257 | Socket pSocket) throws IOException { |
|
258 | 175 | return new Connection(pServer, pXmlRpcServer, pSocket); |
259 | } |
|
260 | ||
261 | /** |
|
262 | * Listens for client requests until stopped. Call {@link |
|
263 | * #start()} to invoke this method, and {@link #shutdown()} to |
|
264 | * break out of it. |
|
265 | * |
|
266 | * @throws RuntimeException Generally caused by either an |
|
267 | * <code>UnknownHostException</code> or <code>BindException</code> |
|
268 | * with the vanilla web server. |
|
269 | * |
|
270 | * @see #start() |
|
271 | * @see #shutdown() |
|
272 | */ |
|
273 | public void run() { |
|
274 | 154 | pool = new ThreadPool(server.getMaxThreads(), "XML-RPC"); |
275 | try { |
|
276 | 585 | while (listener != null) { |
277 | try { |
|
278 | 431 | Socket socket = serverSocket.accept(); |
279 | try { |
|
280 | 245 | socket.setTcpNoDelay(true); |
281 | 0 | } catch (SocketException socketOptEx) { |
282 | 0 | log(socketOptEx); |
283 | } |
|
284 | ||
285 | try { |
|
286 | 245 | if (allowConnection(socket)) { |
287 | 245 | final ThreadPool.Task task = newTask(this, server, socket); |
288 | 245 | if (pool.startTask(task)) { |
289 | 245 | socket = null; |
290 | } else { |
|
291 | 0 | log("Maximum load of " + pool.getMaxThreads() |
292 | 0 | + " exceeded, rejecting client"); |
293 | } |
|
294 | } |
|
295 | 0 | } finally { |
296 | 245 | if (socket != null) { try { socket.close(); } catch (Throwable ignore) {} } |
297 | 0 | } |
298 | 32 | } catch (InterruptedIOException checkState) { |
299 | // Timeout while waiting for a client (from |
|
300 | // SO_TIMEOUT)...try again if still listening. |
|
301 | 0 | } catch (Throwable t) { |
302 | 0 | log(t); |
303 | } |
|
304 | } |
|
305 | 0 | } finally { |
306 | 0 | if (serverSocket != null) { |
307 | try { |
|
308 | 0 | serverSocket.close(); |
309 | 0 | } catch (IOException e) { |
310 | 0 | log(e); |
311 | } |
|
312 | } |
|
313 | ||
314 | // Shutdown our Runner-based threads |
|
315 | 0 | pool.shutdown(); |
316 | 0 | } |
317 | 0 | } |
318 | ||
319 | /** |
|
320 | * Stop listening on the server port. Shutting down our {@link |
|
321 | * #listener} effectively breaks it out of its {@link #run()} |
|
322 | * loop. |
|
323 | * |
|
324 | * @see #run() |
|
325 | */ |
|
326 | public synchronized void shutdown() { |
|
327 | // Stop accepting client connections |
|
328 | 0 | if (listener != null) { |
329 | 0 | Thread l = listener; |
330 | 0 | listener = null; |
331 | 0 | l.interrupt(); |
332 | 0 | pool.shutdown(); |
333 | } |
|
334 | 0 | } |
335 | ||
336 | /** Returns the port, on which the web server is running. |
|
337 | * This method may be invoked after {@link #start()} only. |
|
338 | * @return Servers port number |
|
339 | */ |
|
340 | 284 | public int getPort() { return serverSocket.getLocalPort(); } |
341 | ||
342 | /** Logs an error. |
|
343 | * @param pError The error being logged. |
|
344 | */ |
|
345 | public void log(Throwable pError) { |
|
346 | 0 | log.error(pError.getMessage(), pError); |
347 | 0 | } |
348 | ||
349 | /** Logs a message. |
|
350 | * @param pMessage The being logged. |
|
351 | */ |
|
352 | public synchronized void log(String pMessage) { |
|
353 | 0 | log.error(pMessage); |
354 | 0 | } |
355 | ||
356 | /** Returns the {@link org.apache.xmlrpc.server.XmlRpcServer}. |
|
357 | * @return The server object. |
|
358 | */ |
|
359 | public XmlRpcStreamServer getXmlRpcServer() { |
|
360 | 110 | return server; |
361 | } |
|
362 | } |