View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.gateway.kernel.rest.client;
21  
22  import io.netty.bootstrap.Bootstrap;
23  import io.netty.buffer.ByteBuf;
24  import io.netty.buffer.Unpooled;
25  import io.netty.channel.Channel;
26  import io.netty.channel.ChannelFuture;
27  import io.netty.channel.ChannelInitializer;
28  import io.netty.channel.EventLoopGroup;
29  import io.netty.channel.nio.NioEventLoopGroup;
30  import io.netty.channel.socket.SocketChannel;
31  import io.netty.handler.codec.http.DefaultFullHttpRequest;
32  import io.netty.handler.codec.http.DefaultHttpRequest;
33  import io.netty.handler.codec.http.FullHttpRequest;
34  import io.netty.handler.codec.http.HttpHeaderNames;
35  import io.netty.handler.codec.http.HttpHeaderValues;
36  import io.netty.handler.codec.http.HttpHeaders;
37  import io.netty.handler.codec.http.HttpMethod;
38  import io.netty.handler.codec.http.HttpRequest;
39  import io.netty.handler.codec.http.HttpVersion;
40  import io.netty.handler.codec.http.QueryStringEncoder;
41  import org.joda.time.DateTime;
42  import org.waarp.common.crypto.HmacSha256;
43  import org.waarp.common.crypto.ssl.WaarpSslUtility;
44  import org.waarp.common.exception.CryptoException;
45  import org.waarp.common.logging.WaarpLogger;
46  import org.waarp.common.logging.WaarpLoggerFactory;
47  import org.waarp.common.logging.WaarpSlf4JLoggerFactory;
48  import org.waarp.common.utility.WaarpNettyUtil;
49  import org.waarp.common.utility.WaarpStringUtils;
50  import org.waarp.common.utility.WaarpThreadFactory;
51  import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
52  import org.waarp.gateway.kernel.rest.RestArgument;
53  
54  import java.io.File;
55  import java.io.IOException;
56  import java.net.InetSocketAddress;
57  import java.net.URI;
58  import java.net.URISyntaxException;
59  import java.util.Map;
60  import java.util.Map.Entry;
61  
62  /**
63   * Http Rest Client helper
64   */
65  public class HttpRestClientHelper {
66    private static final String
67        NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON =
68        "Need more arguments: http://host:port/uri method user pwd sign=path|nosign [json]";
69  
70    private static WaarpLogger logger;
71  
72    private final Bootstrap bootstrap;
73  
74    private final HttpHeaders headers;
75  
76    private String baseUri = "/";
77  
78    /**
79     * @param baseUri base of all URI, in general simply "/" (default if
80     *     null)
81     * @param nbclient max number of client connected at once
82     * @param timeout timeout used in connection
83     * @param initializer the associated client pipeline factory
84     */
85    public HttpRestClientHelper(final String baseUri, final int nbclient,
86                                final long timeout,
87                                final ChannelInitializer<SocketChannel> initializer) {
88      if (logger == null) {
89        logger = WaarpLoggerFactory.getLogger(HttpRestClientHelper.class);
90      }
91      if (baseUri != null) {
92        this.baseUri = baseUri;
93      }
94      // Configure the client.
95      bootstrap = new Bootstrap();
96      /*
97       * ExecutorService Worker Boss
98       */
99      final EventLoopGroup workerGroup = new NioEventLoopGroup(nbclient,
100                                                              new WaarpThreadFactory(
101                                                                  "Rest_" +
102                                                                  baseUri +
103                                                                  '_'));
104     WaarpNettyUtil.setBootstrap(bootstrap, workerGroup, 30000);
105     // Configure the pipeline factory.
106     bootstrap.handler(initializer);
107 
108     // will ignore real request
109     final HttpRequest request =
110         new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, baseUri);
111     headers = request.headers();
112     headers.set(HttpHeaderNames.ACCEPT_ENCODING,
113                 HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE);
114     headers.set(HttpHeaderNames.ACCEPT_CHARSET, "utf-8;q=0.7,*;q=0.7");
115     headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "fr,en");
116     headers.set(HttpHeaderNames.USER_AGENT,
117                 "Netty Simple Http Rest Client side");
118     headers.set(HttpHeaderNames.ACCEPT,
119                 "text/html,text/plain,application/xhtml+xml,application/xml,application/json;q=0.9,*/*;q=0.8");
120     // connection will not close but needed
121     /*
122      * request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)
123      */
124     // request.setHeader("Connection","keep-alive")
125     // request.setHeader("Keep-Alive","300")
126   }
127 
128   /**
129    * Create one new connection to the remote host using port
130    *
131    * @param host
132    * @param port
133    *
134    * @return the channel if connected or Null if not
135    */
136   public final Channel getChannel(final String host, final int port) {
137     // Start the connection attempt.
138     final ChannelFuture future =
139         bootstrap.connect(new InetSocketAddress(host, port));
140     // Wait until the connection attempt succeeds or fails.
141     final Channel channel = WaarpSslUtility.waitforChannelReady(future);
142     if (channel != null) {
143       final RestFuture futureChannel = new RestFuture(true);
144       channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT)
145              .set(futureChannel);
146     }
147     return channel;
148   }
149 
150   /**
151    * Send an HTTP query using the channel for target, using signature
152    *
153    * @param hmacSha256 SHA-256 key to create the signature
154    * @param channel target of the query
155    * @param method HttpMethod to use
156    * @param host target of the query (shall be the same as for the
157    *     channel)
158    * @param addedUri additional uri, added to baseUri (shall include
159    *     also
160    *     extra arguments) (might be null)
161    * @param user user to use in authenticated Rest procedure (might be
162    *     null)
163    * @param pwd password to use in authenticated Rest procedure (might
164    *     be
165    *     null)
166    * @param uriArgs arguments for Uri if any (might be null)
167    * @param json json to send as body in the request (might be null);
168    *     Useful
169    *     in PUT, POST but should not
170    *     in GET, DELETE, OPTIONS
171    *
172    * @return the RestFuture associated with this request
173    */
174   public final RestFuture sendQuery(final HmacSha256 hmacSha256,
175                                     final Channel channel,
176                                     final HttpMethod method, final String host,
177                                     final String addedUri, final String user,
178                                     final String pwd,
179                                     final Map<String, String> uriArgs,
180                                     final String json) {
181     // Prepare the HTTP request.
182     logger.debug("Prepare request: {}:{}:{}", method, addedUri, json);
183     final RestFuture future =
184         channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT).get();
185     final QueryStringEncoder encoder;
186     if (addedUri != null) {
187       encoder = new QueryStringEncoder(baseUri + addedUri);
188     } else {
189       encoder = new QueryStringEncoder(baseUri);
190     }
191     // add Form attribute
192     if (uriArgs != null) {
193       for (final Entry<String, String> elt : uriArgs.entrySet()) {
194         encoder.addParam(elt.getKey(), elt.getValue());
195       }
196     }
197     final String[] result;
198     try {
199       result = RestArgument.getBaseAuthent(hmacSha256, encoder, user, pwd);
200       logger.debug("Authent encoded");
201     } catch (final HttpInvalidAuthenticationException e) {
202       logger.error(e.getMessage());
203       future.setFailure(e);
204       return future;
205     }
206     final URI uri;
207     try {
208       uri = encoder.toUri();
209     } catch (final URISyntaxException e) {
210       logger.error(e.getMessage());
211       future.setFailure(e);
212       return future;
213     }
214     if (logger.isDebugEnabled()) {
215       logger.debug("Uri ready: {}", uri.toASCIIString());
216     }
217     final FullHttpRequest request;
218     if (json != null) {
219       logger.debug("Add body");
220       final ByteBuf buffer =
221           Unpooled.wrappedBuffer(json.getBytes(WaarpStringUtils.UTF8));
222       request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
223                                            uri.toASCIIString(), buffer);
224       request.headers()
225              .set(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes());
226     } else {
227       request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
228                                            uri.toASCIIString());
229     }
230     // it is legal to add directly header or cookie into the request until finalize
231     request.headers().add(headers);
232     request.headers().set(HttpHeaderNames.HOST, host);
233     if (user != null) {
234       request.headers().set(
235           (CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
236           user);
237     }
238     request.headers().set(
239         (CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
240         result[0]);
241     request.headers().set(
242         (CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_KEY.field,
243         result[1]);
244     // send request
245     logger.debug("Send request");
246     channel.writeAndFlush(request);
247     logger.debug("Request sent");
248     return future;
249   }
250 
251   /**
252    * Send an HTTP query using the channel for target, but without any
253    * Signature
254    *
255    * @param channel target of the query
256    * @param method HttpMethod to use
257    * @param host target of the query (shall be the same as for the
258    *     channel)
259    * @param addedUri additional uri, added to baseUri (shall include
260    *     also
261    *     extra arguments) (might be null)
262    * @param user user to use in authenticated Rest procedure (might be
263    *     null)
264    * @param uriArgs arguments for Uri if any (might be null)
265    * @param json json to send as body in the request (might be null);
266    *     Useful
267    *     in PUT, POST but should not in
268    *     GET, DELETE, OPTIONS
269    *
270    * @return the RestFuture associated with this request
271    */
272   public final RestFuture sendQuery(final Channel channel,
273                                     final HttpMethod method, final String host,
274                                     final String addedUri, final String user,
275                                     final Map<String, String> uriArgs,
276                                     final String json) {
277     // Prepare the HTTP request.
278     logger.debug("Prepare request: {}:{}:{}", method, addedUri, json);
279     final RestFuture future =
280         channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT).get();
281     final QueryStringEncoder encoder;
282     if (addedUri != null) {
283       encoder = new QueryStringEncoder(baseUri + addedUri);
284     } else {
285       encoder = new QueryStringEncoder(baseUri);
286     }
287     // add Form attribute
288     if (uriArgs != null) {
289       for (final Entry<String, String> elt : uriArgs.entrySet()) {
290         encoder.addParam(elt.getKey(), elt.getValue());
291       }
292     }
293     final URI uri;
294     try {
295       uri = encoder.toUri();
296     } catch (final URISyntaxException e) {
297       logger.error(e.getMessage());
298       future.setFailure(e);
299       return future;
300     }
301     if (logger.isDebugEnabled()) {
302       logger.debug("Uri ready: " + uri.toASCIIString());
303     }
304     final FullHttpRequest request;
305     if (json != null) {
306       logger.debug("Add body");
307       final ByteBuf buffer =
308           Unpooled.wrappedBuffer(json.getBytes(WaarpStringUtils.UTF8));
309       request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
310                                            uri.toASCIIString(), buffer);
311       request.headers()
312              .set(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes());
313     } else {
314       request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
315                                            uri.toASCIIString());
316     }
317 
318     // it is legal to add directly header or cookie into the request until finalize
319     request.headers().add(headers);
320     request.headers().set(HttpHeaderNames.HOST, host);
321     if (user != null) {
322       request.headers().set(
323           (CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
324           user);
325     }
326     request.headers().set(
327         (CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
328         new DateTime().toString());
329     // send request
330     logger.debug("Send request");
331     channel.writeAndFlush(request);
332     logger.debug("Request sent");
333     return future;
334   }
335 
336   /**
337    * Finalize the HttpRestClientHelper
338    */
339   public final void closeAll() {
340     bootstrap.config().group().shutdownGracefully();
341   }
342 
343   /**
344    * @param args as uri (http://host:port/uri method user pwd
345    *     sign=path|nosign [json])
346    */
347   public static void main(final String[] args) {
348     WaarpLoggerFactory.setDefaultFactoryIfNotSame(
349         new WaarpSlf4JLoggerFactory(null));
350     logger = WaarpLoggerFactory.getLogger(HttpRestClientHelper.class);
351     if (args.length < 5) {
352       logger.error(
353           NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
354       return;
355     }
356     final String uri = args[0];
357     final String meth = args[1];
358     final String user = args[2];
359     final String pwd = args[3];
360     final boolean sign = args[4].toLowerCase().contains("sign=");
361     HmacSha256 hmacSha256 = null;
362     if (sign) {
363       final String file = args[4].replace("sign=", "");
364       hmacSha256 = new HmacSha256();
365       try {
366         hmacSha256.setSecretKey(new File(file));
367       } catch (final CryptoException e) {
368         logger.error(
369             NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
370         return;
371       } catch (final IOException e) {
372         logger.error(
373             NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
374         return;
375       }
376     }
377     String json = null;
378     if (args.length > 5) {
379       json = args[5].replace("'", "\"");
380     }
381     final HttpMethod method = HttpMethod.valueOf(meth);
382     int port = -1;
383     final String host;
384     final String path;
385     try {
386       final URI realUri = new URI(uri);
387       port = realUri.getPort();
388       host = realUri.getHost();
389       path = realUri.getPath();
390     } catch (final URISyntaxException e) {
391       logger.error("Error: {}", e.getMessage());
392       return;
393     }
394     final HttpRestClientHelper client = new HttpRestClientHelper(path, 1, 30000,
395                                                                  new HttpRestClientSimpleInitializer());
396     final Channel channel = client.getChannel(host, port);
397     if (channel == null) {
398       client.closeAll();
399       logger.error("Cannot connect to " + host + " on port " + port);
400       return;
401     }
402     final RestFuture future;
403     if (sign) {
404       future =
405           client.sendQuery(hmacSha256, channel, method, host, null, user, pwd,
406                            null, json);
407     } else {
408       future = client.sendQuery(channel, method, host, null, user, null, json);
409     }
410     future.awaitOrInterruptible();
411     WaarpSslUtility.closingSslChannel(channel);
412     if (future.isSuccess()) {
413       logger.warn(future.getRestArgument().prettyPrint());
414     } else {
415       final RestArgument ra = future.getRestArgument();
416       if (ra != null) {
417         logger.error(ra.prettyPrint());
418       } else {
419         logger.error("Query in error", future.getCause());
420       }
421     }
422     client.closeAll();
423   }
424 }