HttpRestClientHelper.java
/*
* This file is part of Waarp Project (named also Waarp or GG).
*
* Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
* tags. See the COPYRIGHT.txt in the distribution for a full listing of
* individual contributors.
*
* All Waarp Project is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Waarp . If not, see <http://www.gnu.org/licenses/>.
*/
package org.waarp.gateway.kernel.rest.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringEncoder;
import org.joda.time.DateTime;
import org.waarp.common.crypto.HmacSha256;
import org.waarp.common.crypto.ssl.WaarpSslUtility;
import org.waarp.common.exception.CryptoException;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;
import org.waarp.common.logging.WaarpSlf4JLoggerFactory;
import org.waarp.common.utility.WaarpNettyUtil;
import org.waarp.common.utility.WaarpStringUtils;
import org.waarp.common.utility.WaarpThreadFactory;
import org.waarp.gateway.kernel.exception.HttpInvalidAuthenticationException;
import org.waarp.gateway.kernel.rest.RestArgument;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Map.Entry;
/**
* Http Rest Client helper
*/
public class HttpRestClientHelper {
private static final String
NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON =
"Need more arguments: http://host:port/uri method user pwd sign=path|nosign [json]";
private static WaarpLogger logger;
private final Bootstrap bootstrap;
private final HttpHeaders headers;
private String baseUri = "/";
/**
* @param baseUri base of all URI, in general simply "/" (default if
* null)
* @param nbclient max number of client connected at once
* @param timeout timeout used in connection
* @param initializer the associated client pipeline factory
*/
public HttpRestClientHelper(final String baseUri, final int nbclient,
final long timeout,
final ChannelInitializer<SocketChannel> initializer) {
if (logger == null) {
logger = WaarpLoggerFactory.getLogger(HttpRestClientHelper.class);
}
if (baseUri != null) {
this.baseUri = baseUri;
}
// Configure the client.
bootstrap = new Bootstrap();
/*
* ExecutorService Worker Boss
*/
final EventLoopGroup workerGroup = new NioEventLoopGroup(nbclient,
new WaarpThreadFactory(
"Rest_" +
baseUri +
'_'));
WaarpNettyUtil.setBootstrap(bootstrap, workerGroup, 30000);
// Configure the pipeline factory.
bootstrap.handler(initializer);
// will ignore real request
final HttpRequest request =
new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, baseUri);
headers = request.headers();
headers.set(HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP + "," + HttpHeaderValues.DEFLATE);
headers.set(HttpHeaderNames.ACCEPT_CHARSET, "utf-8;q=0.7,*;q=0.7");
headers.set(HttpHeaderNames.ACCEPT_LANGUAGE, "fr,en");
headers.set(HttpHeaderNames.USER_AGENT,
"Netty Simple Http Rest Client side");
headers.set(HttpHeaderNames.ACCEPT,
"text/html,text/plain,application/xhtml+xml,application/xml,application/json;q=0.9,*/*;q=0.8");
// connection will not close but needed
/*
* request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)
*/
// request.setHeader("Connection","keep-alive")
// request.setHeader("Keep-Alive","300")
}
/**
* Create one new connection to the remote host using port
*
* @param host
* @param port
*
* @return the channel if connected or Null if not
*/
public final Channel getChannel(final String host, final int port) {
// Start the connection attempt.
final ChannelFuture future =
bootstrap.connect(new InetSocketAddress(host, port));
// Wait until the connection attempt succeeds or fails.
final Channel channel = WaarpSslUtility.waitforChannelReady(future);
if (channel != null) {
final RestFuture futureChannel = new RestFuture(true);
channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT)
.set(futureChannel);
}
return channel;
}
/**
* Send an HTTP query using the channel for target, using signature
*
* @param hmacSha256 SHA-256 key to create the signature
* @param channel target of the query
* @param method HttpMethod to use
* @param host target of the query (shall be the same as for the
* channel)
* @param addedUri additional uri, added to baseUri (shall include
* also
* extra arguments) (might be null)
* @param user user to use in authenticated Rest procedure (might be
* null)
* @param pwd password to use in authenticated Rest procedure (might
* be
* null)
* @param uriArgs arguments for Uri if any (might be null)
* @param json json to send as body in the request (might be null);
* Useful
* in PUT, POST but should not
* in GET, DELETE, OPTIONS
*
* @return the RestFuture associated with this request
*/
public final RestFuture sendQuery(final HmacSha256 hmacSha256,
final Channel channel,
final HttpMethod method, final String host,
final String addedUri, final String user,
final String pwd,
final Map<String, String> uriArgs,
final String json) {
// Prepare the HTTP request.
logger.debug("Prepare request: {}:{}:{}", method, addedUri, json);
final RestFuture future =
channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT).get();
final QueryStringEncoder encoder;
if (addedUri != null) {
encoder = new QueryStringEncoder(baseUri + addedUri);
} else {
encoder = new QueryStringEncoder(baseUri);
}
// add Form attribute
if (uriArgs != null) {
for (final Entry<String, String> elt : uriArgs.entrySet()) {
encoder.addParam(elt.getKey(), elt.getValue());
}
}
final String[] result;
try {
result = RestArgument.getBaseAuthent(hmacSha256, encoder, user, pwd);
logger.debug("Authent encoded");
} catch (final HttpInvalidAuthenticationException e) {
logger.error(e.getMessage());
future.setFailure(e);
return future;
}
final URI uri;
try {
uri = encoder.toUri();
} catch (final URISyntaxException e) {
logger.error(e.getMessage());
future.setFailure(e);
return future;
}
if (logger.isDebugEnabled()) {
logger.debug("Uri ready: {}", uri.toASCIIString());
}
final FullHttpRequest request;
if (json != null) {
logger.debug("Add body");
final ByteBuf buffer =
Unpooled.wrappedBuffer(json.getBytes(WaarpStringUtils.UTF8));
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
uri.toASCIIString(), buffer);
request.headers()
.set(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes());
} else {
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
uri.toASCIIString());
}
// it is legal to add directly header or cookie into the request until finalize
request.headers().add(headers);
request.headers().set(HttpHeaderNames.HOST, host);
if (user != null) {
request.headers().set(
(CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
user);
}
request.headers().set(
(CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
result[0]);
request.headers().set(
(CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_KEY.field,
result[1]);
// send request
logger.debug("Send request");
channel.writeAndFlush(request);
logger.debug("Request sent");
return future;
}
/**
* Send an HTTP query using the channel for target, but without any
* Signature
*
* @param channel target of the query
* @param method HttpMethod to use
* @param host target of the query (shall be the same as for the
* channel)
* @param addedUri additional uri, added to baseUri (shall include
* also
* extra arguments) (might be null)
* @param user user to use in authenticated Rest procedure (might be
* null)
* @param uriArgs arguments for Uri if any (might be null)
* @param json json to send as body in the request (might be null);
* Useful
* in PUT, POST but should not in
* GET, DELETE, OPTIONS
*
* @return the RestFuture associated with this request
*/
public final RestFuture sendQuery(final Channel channel,
final HttpMethod method, final String host,
final String addedUri, final String user,
final Map<String, String> uriArgs,
final String json) {
// Prepare the HTTP request.
logger.debug("Prepare request: {}:{}:{}", method, addedUri, json);
final RestFuture future =
channel.attr(HttpRestClientSimpleResponseHandler.RESTARGUMENT).get();
final QueryStringEncoder encoder;
if (addedUri != null) {
encoder = new QueryStringEncoder(baseUri + addedUri);
} else {
encoder = new QueryStringEncoder(baseUri);
}
// add Form attribute
if (uriArgs != null) {
for (final Entry<String, String> elt : uriArgs.entrySet()) {
encoder.addParam(elt.getKey(), elt.getValue());
}
}
final URI uri;
try {
uri = encoder.toUri();
} catch (final URISyntaxException e) {
logger.error(e.getMessage());
future.setFailure(e);
return future;
}
if (logger.isDebugEnabled()) {
logger.debug("Uri ready: " + uri.toASCIIString());
}
final FullHttpRequest request;
if (json != null) {
logger.debug("Add body");
final ByteBuf buffer =
Unpooled.wrappedBuffer(json.getBytes(WaarpStringUtils.UTF8));
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
uri.toASCIIString(), buffer);
request.headers()
.set(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes());
} else {
request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method,
uri.toASCIIString());
}
// it is legal to add directly header or cookie into the request until finalize
request.headers().add(headers);
request.headers().set(HttpHeaderNames.HOST, host);
if (user != null) {
request.headers().set(
(CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_USER.field,
user);
}
request.headers().set(
(CharSequence) RestArgument.REST_ROOT_FIELD.ARG_X_AUTH_TIMESTAMP.field,
new DateTime().toString());
// send request
logger.debug("Send request");
channel.writeAndFlush(request);
logger.debug("Request sent");
return future;
}
/**
* Finalize the HttpRestClientHelper
*/
public final void closeAll() {
bootstrap.config().group().shutdownGracefully();
}
/**
* @param args as uri (http://host:port/uri method user pwd
* sign=path|nosign [json])
*/
public static void main(final String[] args) {
WaarpLoggerFactory.setDefaultFactoryIfNotSame(
new WaarpSlf4JLoggerFactory(null));
logger = WaarpLoggerFactory.getLogger(HttpRestClientHelper.class);
if (args.length < 5) {
logger.error(
NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
return;
}
final String uri = args[0];
final String meth = args[1];
final String user = args[2];
final String pwd = args[3];
final boolean sign = args[4].toLowerCase().contains("sign=");
HmacSha256 hmacSha256 = null;
if (sign) {
final String file = args[4].replace("sign=", "");
hmacSha256 = new HmacSha256();
try {
hmacSha256.setSecretKey(new File(file));
} catch (final CryptoException e) {
logger.error(
NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
return;
} catch (final IOException e) {
logger.error(
NEED_MORE_ARGUMENTS_HTTP_HOST_PORT_URI_METHOD_USER_SIGN_PATH_NOSIGN_JSON);
return;
}
}
String json = null;
if (args.length > 5) {
json = args[5].replace("'", "\"");
}
final HttpMethod method = HttpMethod.valueOf(meth);
int port = -1;
final String host;
final String path;
try {
final URI realUri = new URI(uri);
port = realUri.getPort();
host = realUri.getHost();
path = realUri.getPath();
} catch (final URISyntaxException e) {
logger.error("Error: {}", e.getMessage());
return;
}
final HttpRestClientHelper client = new HttpRestClientHelper(path, 1, 30000,
new HttpRestClientSimpleInitializer());
final Channel channel = client.getChannel(host, port);
if (channel == null) {
client.closeAll();
logger.error("Cannot connect to " + host + " on port " + port);
return;
}
final RestFuture future;
if (sign) {
future =
client.sendQuery(hmacSha256, channel, method, host, null, user, pwd,
null, json);
} else {
future = client.sendQuery(channel, method, host, null, user, null, json);
}
future.awaitOrInterruptible();
WaarpSslUtility.closingSslChannel(channel);
if (future.isSuccess()) {
logger.warn(future.getRestArgument().prettyPrint());
} else {
final RestArgument ra = future.getRestArgument();
if (ra != null) {
logger.error(ra.prettyPrint());
} else {
logger.error("Query in error", future.getCause());
}
}
client.closeAll();
}
}