1   package com.trendmicro.grid.acl.client;
2   
3   import com.trendmicro.grid.acl.PublicServiceDiscovery;
4   import com.trendmicro.grid.acl.Service;
5   import com.trendmicro.grid.acl.l0.*;
6   import com.trendmicro.grid.acl.l0.datatypes.FileIdentifier;
7   import net.sf.tinyjee.streams.ByteBufferOutputStream;
8   import net.sf.tinyjee.ws.client.ClientServiceContext;
9   
10  import javax.xml.ws.WebServiceFeature;
11  import java.io.File;
12  import java.io.IOException;
13  import java.io.InputStream;
14  import java.io.OutputStream;
15  import java.net.*;
16  import java.security.DigestInputStream;
17  import java.security.MessageDigest;
18  import java.security.NoSuchAlgorithmException;
19  import java.util.Collection;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.UUID;
23  
24  import static java.util.logging.Level.WARNING;
25  import static java.util.logging.Logger.getLogger;
26  
27  /**
28   * Implements a sharable, thread safe client to connect to the JAX-WS based web services.
29   *
30   * @author Juergen_Kellerer, 2010-05-16
31   */
32  public class ServiceClient {
33  
34  	static final int STREAM_BUFFER_SIZE = 8 * 1024;
35  
36  	/**
37  	 * HACK: Hiding info logs on dynamically compiled client stubs as they pollute the
38  	 * logs (especially when used in a CLI context).
39  	 */
40  	static {
41  		if (!Boolean.getBoolean("gacl.jaxws.verbose")) {
42  			getLogger("com.sun.xml.ws").setLevel(WARNING);
43  			getLogger("javax.enterprise.resource.webservices").setLevel(WARNING);
44  		}
45  	}
46  
47  	private final String defaultHost;
48  	private int defaultPortNumber;
49  	private boolean defaultSecure;
50  
51  	private final ClientServiceContext context = new ClientServiceContext();
52  
53  	/**
54  	 * Creates a new service client using built-in default settings.
55  	 */
56  	public ServiceClient() {
57  		this(null, null);
58  	}
59  
60  	/**
61  	 * Creates a new service client using the given GRID ACL default host and an optional api-key.
62  	 *
63  	 * @param defaultHost the default hostname to use when retrieving a port using {@link #getPort(Class)}.
64  	 * @param apiKey      an optional API key to use with SOAP calls (can be set to 'null' if not relevant).
65  	 *                    If specified, connections are by default opened using a secured (SSL/TLS) connection.
66  	 */
67  	public ServiceClient(String defaultHost, UUID apiKey) {
68  		// Level0 - Services
69  		addDefinition(CacheControlService.class, "/ws/level-0/internal/cache-control", apiKey);
70  		addDefinition(FileRequestService.class, "/ws/level-0/internal/filecontent-request", apiKey);
71  		addDefinition(FileService.class, "/ws/level-0/internal/files", apiKey);
72  		addDefinition(PackageService.class, "/ws/level-0/internal/packages", apiKey);
73  		addDefinition(ProcessingService.class, "/ws/level-0/internal/processing", apiKey);
74  		addDefinition(PublicAuthenticationService.class, "/ws/level-0/authentication", apiKey);
75  		addDefinition(PublicCategoryService.class, "/ws/level-0/categories", apiKey);
76  		addDefinition(PublicFileService.class, "/ws/level-0/files", apiKey);
77  		addDefinition(PublicPackageService.class, "/ws/level-0/packages", apiKey);
78  		addDefinition(PublicReportService.class, "/ws/level-0/reporting", apiKey);
79  		addDefinition(SourceService.class, "/ws/level-0/internal/sources", apiKey);
80  
81  		// Other - Services
82  		addDefinition(PublicServiceDiscovery.class, "/ws/discovery", apiKey);
83  
84  		this.defaultHost = defaultHost;
85  		defaultSecure = apiKey != null;
86  	}
87  
88  	/**
89  	 * Creates a new service client for the given default hostname, port number and SSL option.
90  	 *
91  	 * @param defaultHost       the default hostname to use when retrieving a port using {@link #getPort(Class)}.
92  	 * @param defaultPortNumber the port number of the ACL running on the given host. Use '0' to use the protocol's default port.
93  	 * @param defaultSecure     toggles whether the service is by default accessed using a secured (SSL/TLS) connection.
94  	 */
95  	public ServiceClient(String defaultHost, int defaultPortNumber, boolean defaultSecure) {
96  		this(defaultHost, defaultPortNumber, defaultSecure, null);
97  	}
98  
99  	/**
100 	 * Creates a new service client for the given default hostname, port number and SSL option.
101 	 *
102 	 * @param defaultHost       the default hostname to use when retrieving a port using {@link #getPort(Class)}.
103 	 * @param defaultPortNumber the port number of the ACL running on the given host. Use '0' to use the protocol's default port.
104 	 * @param defaultSecure     toggles whether the service is by default accessed using a secured (SSL/TLS) connection.
105 	 * @param apiKey            an optional API key to use with SOAP calls. (can be set to 'null')
106 	 */
107 	public ServiceClient(String defaultHost, int defaultPortNumber, boolean defaultSecure, UUID apiKey) {
108 		this(defaultHost, apiKey);
109 		this.defaultPortNumber = defaultPortNumber;
110 		this.defaultSecure = defaultSecure;
111 	}
112 
113 	private void addDefinition(Class<? extends Service> endpointInterface, String path, UUID apiKey) {
114 		if (!path.startsWith("/")) path = '/' + path;
115 		if (apiKey != null) path += ";api-key=" + apiKey;
116 
117 		context.addDefinition(endpointInterface, endpointInterface.getSimpleName(), URI.create("http://localhost" + path + "?wsdl"));
118 	}
119 
120 	/**
121 	 * Returns the underlying client context instance.
122 	 *
123 	 * @return the underlying client context instance.
124 	 */
125 	public ClientServiceContext getContext() {
126 		return context;
127 	}
128 
129 	/**
130 	 * Is a utility method that generates a file identifier from the given URL.
131 	 *
132 	 * @param source the source to generate the file-identifier of.
133 	 * @return a fully qualified file identifier containing SHA1 and MD5.
134 	 * @throws IOException in case of reading the source failed.
135 	 */
136 	public static FileIdentifier generateFileIdentifier(URL source) throws IOException {
137 		final MessageDigest sha1, md5;
138 		try {
139 			sha1 = MessageDigest.getInstance("SHA1");
140 			md5 = MessageDigest.getInstance("MD5");
141 		} catch (NoSuchAlgorithmException e) {
142 			throw new IOException(e);
143 		}
144 
145 		final DigestInputStream sha1Stream = new DigestInputStream(source.openStream(), sha1);
146 		final DigestInputStream md5Stream = new DigestInputStream(sha1Stream, md5);
147 
148 		copyAndClose(md5Stream, null);
149 
150 		return new FileIdentifier(sha1Stream.getMessageDigest().digest(), md5Stream.getMessageDigest().digest());
151 	}
152 
153 	/**
154 	 * Is a utility method that generates a file identifier from the given File.
155 	 *
156 	 * @param source the source to generate the file-identifier of.
157 	 * @return a fully qualified file identifier containing SHA1 and MD5.
158 	 * @throws IOException in case of reading the source failed.
159 	 */
160 	public static FileIdentifier generateFileIdentifier(File source) throws IOException {
161 		return generateFileIdentifier(source.toURI().toURL());
162 	}
163 
164 	/**
165 	 * Gets content from the given URL using the same session as used with web-service calls.
166 	 *
167 	 * @param sourceUrl the URL to GET.
168 	 * @return the input stream containing the content stream.
169 	 * @throws IOException in case of reading the content failed.
170 	 */
171 	public InputStream getFromUrl(URL sourceUrl) throws IOException {
172 		final URLConnection urlConnection = sourceUrl.openConnection();
173 		configureWithSessionCookies(urlConnection);
174 
175 		return urlConnection.getInputStream();
176 	}
177 
178 	/**
179 	 * Gets content from the given URL using the same session as used with web-service calls and writes it to the provided output stream.
180 	 * <p/>
181 	 * This method has more advanced error reporting than {@link #getFromUrl(java.net.URL)} as it also evaluates HTTP response codes and
182 	 * error replies from the server.
183 	 *
184 	 * @param sourceUrl    the URL to GET.
185 	 * @param targetStream the target to write the content to.
186 	 * @throws IOException In case of reading or writing the content failed.
187 	 */
188 	public void getFromUrl(URL sourceUrl, OutputStream targetStream) throws IOException {
189 		final HttpURLConnection huc = (HttpURLConnection) sourceUrl.openConnection();
190 		configureWithSessionCookies(huc);
191 
192 		try {
193 			copyAndClose(huc.getInputStream(), targetStream);
194 			if (huc.getResponseCode() >= 400) throw new IOException("Unexpected response code, should be < 400");
195 		} catch (IOException e) {
196 			String message = "Failed to GET content from URL '" + sourceUrl + '\'';
197 			convertHttpErrorStream(huc, message, e);
198 		}
199 	}
200 
201 	/**
202 	 * Puts content to the given URI using the same session as used with web-service calls.
203 	 *
204 	 * @param inputStream the stream of the content to put.
205 	 * @param targetUrl   the target URL to put the content to.
206 	 * @throws IOException In case of reading or writing the content failed.
207 	 */
208 	public void putToUrl(InputStream inputStream, URL targetUrl) throws IOException {
209 		HttpURLConnection huc = preparePutToUrl(targetUrl);
210 
211 		try {
212 			copyAndClose(inputStream, huc.getOutputStream());
213 			if (huc.getResponseCode() >= 400) throw new IOException("Unexpected response code, should be < 400");
214 		} catch (IOException e) {
215 			String message = "Failed to send content via PUT to URL '" + targetUrl + '\'';
216 			convertHttpErrorStream(huc, message, e);
217 		}
218 	}
219 
220 	/**
221 	 * Prepares a HttpURLConnection to PUT content to the underlying URL using the same session as used with web-service calls.
222 	 *
223 	 * @param targetUrl the target URL to put the content to.
224 	 * @return a configured HttpURLConnection instance that can be used for putting the content.
225 	 * @throws IOException In case of reading or setting the cookies fails.
226 	 */
227 	public HttpURLConnection preparePutToUrl(URL targetUrl) throws IOException {
228 		HttpURLConnection huc = (HttpURLConnection) targetUrl.openConnection();
229 		huc.setRequestMethod("PUT");
230 		huc.setDoOutput(true);
231 		huc.setChunkedStreamingMode(STREAM_BUFFER_SIZE);
232 
233 		configureWithSessionCookies(huc);
234 
235 		return huc;
236 	}
237 
238 	/**
239 	 * Adds session cookies to the given http url connection.
240 	 *
241 	 * @param urlConnection the url connection to configure.
242 	 * @throws IOException in case of the configuration failed.
243 	 */
244 	public void configureWithSessionCookies(URLConnection urlConnection) throws IOException {
245 		try {
246 			final CookieManager manager = context.getSessionHandler().getCookieManager();
247 			final Map<String, List<String>> cookies = manager.get(urlConnection.getURL().toURI(), urlConnection.getRequestProperties());
248 			for (Map.Entry<String, List<String>> entry : cookies.entrySet()) {
249 				for (String cookieValue : entry.getValue())
250 					urlConnection.addRequestProperty(entry.getKey(), cookieValue);
251 			}
252 		} catch (URISyntaxException e) {
253 			throw new IOException(e);
254 		}
255 	}
256 
257 	public String getDefaultHost() {
258 		return defaultHost;
259 	}
260 
261 	public int getDefaultPortNumber() {
262 		return defaultPortNumber;
263 	}
264 
265 	public boolean isDefaultSecure() {
266 		return defaultSecure;
267 	}
268 
269 	/**
270 	 * Returns a cached port (proxy) of the given endpoint interface using the specified default values for host, port-number and security.
271 	 * <p/>
272 	 * Look after implementations of {@link Service} to see what endpoint interfaces exist and can be used here.
273 	 *
274 	 * @param endpointInterface the interface describing the endpoint protocol.
275 	 * @param <T>               the endpoint interface type.
276 	 * @return a cached port (proxy) of the given endpoint interface.
277 	 */
278 	public <T extends Service> T getPort(Class<T> endpointInterface) {
279 		return context.getPort(endpointInterface, defaultHost, defaultPortNumber, defaultSecure);
280 	}
281 
282 	/**
283 	 * Returns a cached port (proxy) of the given endpoint interface.
284 	 * <p/>
285 	 * Look after implementations of {@link Service} to see what endpoint interfaces exist and can be used here.
286 	 *
287 	 * @param endpointInterface the interface describing the endpoint protocol.
288 	 * @param host              the hostname of the remote machine to connect to.
289 	 * @param <T>               the endpoint interface type.
290 	 * @return a cached port (proxy) of the given endpoint interface.
291 	 */
292 	public <T extends Service> T getPort(Class<T> endpointInterface, String host) {
293 		return context.getPort(endpointInterface, host);
294 	}
295 
296 	/**
297 	 * Returns a cached port (proxy) of the given endpoint interface.
298 	 * <p/>
299 	 * Look after implementations of {@link Service} to see what endpoint interfaces exist and can be used here.
300 	 *
301 	 * @param endpointInterface the interface describing the endpoint protocol.
302 	 * @param host              the hostname of the remote machine to connect to.
303 	 * @param port              the port number of the remote machine to connect to.
304 	 * @param secure            whether a secure transport should be chosen.
305 	 * @param features          specifies additional standard or vendor specific features that
306 	 *                          may be applied to the returned port proxy.
307 	 * @param <T>               the endpoint interface type.
308 	 * @return a cached port (proxy) of the given endpoint interface.
309 	 */
310 	public <T extends Service> T getPort(Class<T> endpointInterface, String host, int port, boolean secure, WebServiceFeature... features) {
311 		return context.getPort(endpointInterface, host, port, secure, features);
312 	}
313 
314 	/**
315 	 * Returns all endpoint interfaces of the service definitions.
316 	 *
317 	 * @return all endpoint interfaces of the service definitions.
318 	 */
319 	@SuppressWarnings("unchecked")
320 	public Collection<Class<? extends Service>> getDefinedPorts() {
321 		return (Collection) context.getDefinedPorts();
322 	}
323 
324 	private static void copyAndClose(InputStream in, OutputStream out) throws IOException {
325 		try {
326 			int r;
327 			byte[] buffer = new byte[STREAM_BUFFER_SIZE];
328 			while ((r = in.read(buffer)) != -1)
329 				if (out != null) out.write(buffer, 0, r);
330 		} finally {
331 			in.close();
332 			if (out != null) out.close();
333 		}
334 	}
335 
336 	private static void convertHttpErrorStream(HttpURLConnection huc, String message, IOException originalException) throws IOException {
337 		String detailedErrorMessage = "--failed-to-retrieve-error-details--";
338 		try {
339 			ByteBufferOutputStream messageBuffer = new ByteBufferOutputStream();
340 			copyAndClose(huc.getErrorStream(), messageBuffer);
341 			detailedErrorMessage = messageBuffer.toString();
342 		} catch (IOException ignore) {
343 		}
344 
345 		throw new IOException(message + "; The server replied with:\n" +
346 				huc.getResponseCode() + ' ' + huc.getResponseMessage() +
347 				"\n\n" + detailedErrorMessage, originalException);
348 	}
349 }