com.cedarsoftware.util.JsonHttpProxy.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of n-cube Show documentation
Show all versions of n-cube Show documentation
Multi-dimensional Rule Engine
package com.cedarsoftware.util
import com.cedarsoftware.servlet.JsonCommandServlet
import com.cedarsoftware.util.io.JsonReader
import com.cedarsoftware.util.io.JsonWriter
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.apache.http.Header
import org.apache.http.HttpEntity
import org.apache.http.HttpHost
import org.apache.http.HttpResponse
import org.apache.http.StatusLine
import org.apache.http.auth.AuthScope
import org.apache.http.auth.UsernamePasswordCredentials
import org.apache.http.client.AuthCache
import org.apache.http.client.CredentialsProvider
import org.apache.http.client.config.RequestConfig
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.protocol.HttpClientContext
import org.apache.http.conn.routing.HttpRoute
import org.apache.http.entity.ByteArrayEntity
import org.apache.http.impl.auth.BasicScheme
import org.apache.http.impl.client.BasicAuthCache
import org.apache.http.impl.client.BasicCredentialsProvider
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.impl.client.StandardHttpRequestRetryHandler
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager
import org.apache.http.util.EntityUtils
import org.springframework.beans.factory.annotation.Value
import org.springframework.util.FastByteArrayOutputStream
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import java.text.ParseException
import java.util.zip.Deflater
import static com.cedarsoftware.ncube.NCubeConstants.LOG_ARG_LENGTH
import static com.cedarsoftware.util.StringUtilities.isEmpty
import static com.cedarsoftware.util.io.MetaUtils.getLogMessage
import static org.apache.http.HttpHeaders.*
import static org.apache.http.entity.ContentType.APPLICATION_JSON
/**
* @author John DeRegnaucourt ([email protected]), Josh Snyder ([email protected])
*
* Copyright (c) Cedar Software LLC
*
* Licensed under the Apache License, Version 2.0 (the "License")
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Slf4j
@CompileStatic
class JsonHttpProxy implements CallableBean
{
@Value("#{\${ncube.proxy.cookiesToInclude:'JSESSIONID'}.split(',')}")
private List cookiesToInclude
private final CloseableHttpClient httpClient
private CredentialsProvider credsProvider
private AuthCache authCache
private final HttpHost httpHost
private final HttpHost proxyHost
private final String context
private final String username
private final String password
private final int numConnections
private String accessTokenUri
private String token
private long tokenExpireTime = System.currentTimeMillis()
private static final int LOG_RESPONSE_BODY_MAX = 1500 -1
private static final int TOKEN_RETRY_MAX = 2
private static final int TOKEN_RETRY_DELAY = 500
private static final int CALL_RETRY_MAX = 4
private static final int CALL_RETRY_DELAY = 1000
JsonHttpProxy(HttpHost httpHost, String context, String username = null, String password = null, int numConnections = 100)
{
this.httpHost = httpHost
proxyHost = null
this.context = context
this.username = username
this.password = password
this.numConnections = numConnections
httpClient = createClient()
createAuthCache()
log.info('Started with basic authentication')
}
JsonHttpProxy(HttpHost httpHost, HttpHost proxyHost, String context, String username = null, String password = null, int numConnections = 100)
{
this.httpHost = httpHost
this.proxyHost = proxyHost
this.context = context
this.username = username
this.password = password
this.numConnections = numConnections
httpClient = createClient()
createAuthCache()
log.info('Started with basic authentication with proxy host')
}
JsonHttpProxy(HttpHost httpHost, String context, String accessTokenUri, String clientId, String clientSecret, int numConnections = 100)
{
this.httpHost = httpHost
this.proxyHost = null
this.context = context
this.username = clientId
this.password = clientSecret
this.numConnections = numConnections
this.accessTokenUri = accessTokenUri
httpClient = createClient()
log.info('Started with oauth2 authentication')
}
/**
* Creates the client object with the proxy and cookie store for later use.
*
* @return A {@link CloseableHttpClient}
*/
protected CloseableHttpClient createClient()
{
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager()
cm.maxTotal = numConnections // Max total connection
cm.defaultMaxPerRoute = numConnections // Default max connection per route
cm.setMaxPerRoute(new HttpRoute(httpHost), numConnections) // Max connections per route
RequestConfig.Builder configBuilder = RequestConfig.custom()
configBuilder.connectTimeout = 10 * 1000
configBuilder.connectionRequestTimeout = 10 * 1000
configBuilder.socketTimeout = 420 * 1000
RequestConfig config = configBuilder.build()
HttpClientBuilder builder = HttpClientBuilder.create()
builder.defaultRequestConfig = config
builder.connectionManager = cm
builder.disableCookieManagement()
builder.retryHandler = new StandardHttpRequestRetryHandler(5, true)
if (proxyHost)
{
builder.proxy = proxyHost
}
CloseableHttpClient httpClient = builder.build()
return httpClient
}
Object call(String bean, String methodName, List args)
{
Object result = null
int attempt = 0
boolean attemptToCall = true
while (attemptToCall)
{
attempt++
try
{
result = callInternal(bean, methodName, args);
attemptToCall = false
}
catch (TokenDecodeException e)
{
String msg = "Failed to call ${bean}. Attempt ${attempt} of ${CALL_RETRY_MAX}. Error: ${e.localizedMessage}"
if (attempt >= CALL_RETRY_MAX)
{
if (attempt >= CALL_RETRY_MAX)
{
log.error(msg, e)
throw e
}
}
log.warn(msg)
Thread.sleep(CALL_RETRY_DELAY)
}
}
return result
}
private Object callInternal(String bean, String methodName, List args)
{
Object[] params = args.toArray()
FastByteArrayOutputStream stream = new FastByteArrayOutputStream(1024)
JsonWriter writer = new JsonWriter(new AdjustableGZIPOutputStream(stream, Deflater.BEST_SPEED))
writer.write(params)
writer.flush()
writer.close()
if (log.debugEnabled)
{
log.debug("${bean}.${getLogMessage(methodName, params, LOG_ARG_LENGTH)}")
}
HttpClientContext clientContext = HttpClientContext.create()
HttpPost request = new HttpPost("${httpHost.toURI()}/${context}/cmd/${bean}/${methodName}")
if (accessTokenUri)
{
String token = acquireToken()
request.setHeader(AUTHORIZATION, "Bearer ${token}")
}
else if (username && password)
{
clientContext.credentialsProvider = credsProvider
clientContext.authCache = authCache
}
else
{
assignCookieHeader(request)
}
request.setHeader(USER_AGENT, 'ncube')
request.setHeader(ACCEPT, APPLICATION_JSON.mimeType)
request.setHeader(ACCEPT_ENCODING, 'gzip, deflate')
request.setHeader(CONTENT_TYPE, "application/json; charset=UTF-8")
request.setHeader(CONTENT_ENCODING, 'gzip')
request.entity = new ByteArrayEntity(stream.toByteArrayUnsafe(), 0, stream.size())
HttpResponse response = httpClient.execute(request, clientContext)
checkHttpResponse(request, response)
request.entity = null
boolean parsedJsonOk = false
JsonReader reader = null
try
{
reader = new JsonReader(new BufferedInputStream(response.entity.content))
Map envelope = reader.readObject() as Map
parsedJsonOk = true
checkEnvelope(envelope, "${bean}.${methodName}")
return envelope.data
}
catch (ThreadDeath t)
{
throw t
}
catch (Throwable e)
{
if (!parsedJsonOk)
{
Header[] filteredHeaders = filterAuthorizationHeader(response.allHeaders)
log.warn("Failed to process response (code: ${response.statusLine.statusCode}) from server with call: ${bean}.${getLogMessage(methodName, args.toArray(), LOG_ARG_LENGTH)}, headers: ${filteredHeaders}")
}
throw e
}
finally
{
if (reader != null)
{
IOUtilities.close(reader)
}
}
}
private static void checkEnvelope(Map envelope, String name)
{
if (envelope.exception != null)
{
throw (Exception)envelope.exception
}
if (!envelope.status)
{
String msg
if (envelope.data instanceof String)
{
msg = envelope.data
}
else if (envelope.data != null)
{
msg = envelope.data.toString()
}
else
{
msg = 'no extra info provided.'
}
throw new RuntimeException("REST call [${name}] indicated failure on server: ${msg}")
}
}
private void assignCookieHeader(HttpPost proxyRequest)
{
HttpServletRequest servletRequest = JsonCommandServlet.servletRequest.get()
if (servletRequest instanceof HttpServletRequest)
{
Cookie[] cookies = servletRequest.cookies
if (cookies == null)
{
return
}
StringJoiner joiner = new StringJoiner("; ")
for (Cookie cookie: cookies)
{
if (cookiesToInclude.contains(cookie.name))
{
joiner.add("${cookie.name}=${cookie.value}")
}
}
if (joiner.length())
{
proxyRequest.setHeader('Cookie', joiner.toString())
}
}
}
private void createAuthCache()
{
byte[] rocketBytes = [0xf0, 0x9f, 0x9a, 0x80]
String rocket = StringUtilities.createUTF8String(rocketBytes)
log.info("NCUBE storage-server: ${rocket} ${httpHost.schemeName}://${httpHost.hostName}:${httpHost.port}/${context}")
if (username && password)
{
credsProvider = new BasicCredentialsProvider()
AuthScope authScope = new AuthScope(httpHost.hostName, httpHost.port)
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password)
credsProvider.setCredentials(authScope, credentials)
authCache = new BasicAuthCache()
authCache.put(httpHost, new BasicScheme())
}
}
private String acquireToken()
{
long currentTime = System.currentTimeMillis()
if (currentTime >= tokenExpireTime)
{
int attempt = 0
boolean acquired = false
while (!acquired)
{
attempt++
JsonReader reader = null
try
{
HttpPost request = new HttpPost("${accessTokenUri}?grant_type=client_credentials")
String authString = "${username}:${password}".bytes.encodeBase64().toString()
request.setHeader(AUTHORIZATION, "Basic ${authString}")
HttpResponse response = httpClient.execute(request)
checkHttpResponse(request, response)
reader = new JsonReader(new BufferedInputStream(response.entity.content))
Map envelope = reader.readObject() as Map
token = envelope.access_token
long expiresInSeconds = (long) envelope.expires_in
long expiresInMillis = (expiresInSeconds - 10) * 1000
tokenExpireTime = currentTime + expiresInMillis
log.info('Updated the oauth2 token')
acquired = true
}
catch (ThreadDeath t)
{
throw t
}
catch (Throwable e)
{
String msg = "Failed to acquire access token from ${accessTokenUri}. Attempt ${attempt} of ${TOKEN_RETRY_MAX}. ${e.class.simpleName}: ${e.localizedMessage}"
if (attempt >= TOKEN_RETRY_MAX)
{
log.error(msg, e)
throw e
}
log.warn(msg)
Thread.sleep(TOKEN_RETRY_DELAY)
}
finally
{
if (reader != null)
{
IOUtilities.close(reader)
}
}
}
}
return token
}
private void checkHttpResponse(HttpPost request, HttpResponse response)
{
StatusLine statusLine = response.statusLine
boolean is2xxStatusCode = Integer.parseInt(statusLine.statusCode.toString().substring(0, 1)) == 2
if (!is2xxStatusCode)
{
String startLine = "${request.method} ${request.URI} ${request.protocolVersion}"
InetAddress inetAddress = InetAddress.getByName(request.URI.host)
String reasonPhrase = isEmpty(statusLine.reasonPhrase) ? "" : " ${statusLine.reasonPhrase}"
String msg = "Received ${statusLine.protocolVersion} ${statusLine.statusCode}${reasonPhrase} from ${request.URI.host}."
Header[] requestHeaders = filterAuthorizationHeader(request.allHeaders)
Header[] responseHeaders = filterAuthorizationHeader(response.allHeaders)
String responseBody = extractHttpResponseEntity(response.entity)
int maxLen = responseBody == null ? 0 : Math.min(responseBody.length(), LOG_RESPONSE_BODY_MAX)
String contentToPrint = maxLen == 0 ? "" : responseBody?.substring(0, maxLen)
String truncIndicator = responseBody?.length() > LOG_RESPONSE_BODY_MAX ? "" : ""
log.error("""${msg}
Host address: ${inetAddress.hostAddress}
Request start-line: ${startLine}
Request headers: ${requestHeaders}
Response headers: ${responseHeaders}
Response body: ${contentToPrint}${truncIndicator}""")
String responseBodyLc = responseBody.toLowerCase()
if (401 == statusLine.statusCode && responseBodyLc.contains("invalid") && responseBodyLc.contains("decode"))
{ // In case of a network failover to a different token provider, any token generated from the previous
// token provider may fail to be decoded by the service it is presented to. This forces a new token to
// be acquired for the next request.
tokenExpireTime = System.currentTimeMillis()
throw new TokenDecodeException('Failed to decode token.')
}
throw new RuntimeException(msg)
}
}
class TokenDecodeException extends RuntimeException
{
TokenDecodeException(String message)
{
super(message)
}
}
private static String extractHttpResponseEntity(HttpEntity entity)
{
String responseBody
try
{
responseBody = EntityUtils.toString(entity)
}
catch (IOException | ParseException | IllegalArgumentException e)
{
responseBody = ""
log.error("HttpEntity could not be converted to a String. ${e.class.simpleName}: ${e.localizedMessage}", e)
}
return responseBody
}
private static Header[] filterAuthorizationHeader(Header[] headers)
{
if (headers == null)
{
return null
}
List filteredHeaders = []
for (Header header : headers)
{
if (header.name?.toLowerCase() == 'authorization')
{
log.debug("Filtering '${header.name}' header.")
continue
}
filteredHeaders.add(header)
}
return filteredHeaders.toArray(new Header[0])
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy