From 651b5e445023d7d7004a8a7387065454b823c581 Mon Sep 17 00:00:00 2001
From: Thomas Lenz <thomas.lenz@egiz.gv.at>
Date: Wed, 4 Nov 2020 17:50:14 +0100
Subject: refactoring of SL2.0 response processing to mitigate problems with
 ConnectionPool of Apache http-client

---
 .../tasks/AbstractCreateQualEidRequestTask.java    |  49 ++++----
 .../auth/sl20/utils/SL20HttpBindingUtils.java      | 124 +++++++++++++++++++++
 .../auth/sl20/utils/SL20JsonExtractorUtils.java    |  84 --------------
 3 files changed, 153 insertions(+), 104 deletions(-)

(limited to 'eaaf_modules/eaaf_module_auth_sl20/src/main/java')

diff --git a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractCreateQualEidRequestTask.java b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractCreateQualEidRequestTask.java
index 56084d94..9a041028 100644
--- a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractCreateQualEidRequestTask.java
+++ b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/tasks/AbstractCreateQualEidRequestTask.java
@@ -12,6 +12,19 @@ import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.jose4j.base64url.Base64Url;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import at.gv.egiz.eaaf.core.api.idp.IConfigurationWithSP;
 import at.gv.egiz.eaaf.core.api.idp.ISpConfiguration;
 import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext;
@@ -30,22 +43,9 @@ import at.gv.egiz.eaaf.modules.auth.sl20.exceptions.SlCommandoParserException;
 import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20Constants;
 import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20Constants.VdaAuthMethod;
 import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20HttpBindingUtils;
+import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20HttpBindingUtils.Sl20ResponseHolder;
 import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20JsonBuilderUtils;
 import at.gv.egiz.eaaf.modules.auth.sl20.utils.SL20JsonExtractorUtils;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.message.BasicNameValuePair;
-import org.jose4j.base64url.Base64Url;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.i18n.LocaleContextHolder;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
@@ -113,12 +113,21 @@ public abstract class AbstractCreateQualEidRequestTask extends AbstractAuthServl
           Base64Url.encode(sl20Req.toString().getBytes(StandardCharsets.UTF_8)));
 
       // request VDA
-      final HttpResponse httpResp = httpClientFactory.getHttpClient(false).execute(httpReq);
-
+      final Sl20ResponseHolder httpResp = httpClientFactory.getHttpClient(false).execute(
+          httpReq, SL20HttpBindingUtils.sl20ResponseHandler());
+
+      //check on error on http channel
+      if (httpResp.getError() != null) {
+        log.info("Basic SL2.0 response processing has an error. HTTP-StatusCode: {}  ErrorMsg: {}",
+            httpResp.getResponseStatus().getStatusCode(), httpResp.getError().getMessage());
+        throw httpResp.getError();
+        
+      }
+      
       // parse response
       log.info("Receive response from VDA ... ");
-      final JsonNode sl20Resp = SL20JsonExtractorUtils.getSL20ContainerFromResponse(httpResp);
-      final VerificationResult respPayloadContainer = SL20JsonExtractorUtils.extractSL20PayLoad(sl20Resp, null, false);
+      final VerificationResult respPayloadContainer = 
+          SL20JsonExtractorUtils.extractSL20PayLoad(httpResp.getResponseBody(), null, false);
 
       if (respPayloadContainer.isValidSigned() == null) {
         log.debug("Receive unsigned payLoad from VDA");
@@ -139,7 +148,7 @@ public abstract class AbstractCreateQualEidRequestTask extends AbstractAuthServl
             SL20Constants.SL20_COMMAND_PARAM_GENERAL_REDIRECT_SIGNEDCOMMAND, false);
 
         // create forward SL2.0 command
-        final ObjectNode sl20Forward = sl20Resp.deepCopy();
+        final ObjectNode sl20Forward = httpResp.getResponseBody().deepCopy();
         SL20JsonBuilderUtils.addOnlyOnceOfTwo(sl20Forward, SL20Constants.SL20_PAYLOAD, SL20Constants.SL20_SIGNEDPAYLOAD,
             command.deepCopy(), signedCommand);
 
@@ -223,7 +232,7 @@ public abstract class AbstractCreateQualEidRequestTask extends AbstractAuthServl
     final Locale locale = LocaleContextHolder.getLocale();
     final String language = locale.getLanguage();
     if (StringUtils.isNotEmpty(language)) {
-      log.trace("Find i18n context. Inject locale: {} into VDA request", locale.getLanguage());
+      log.trace("Find i18n context). Inject locale: {} into VDA request", locale.getLanguage());
       parameters.add(new BasicNameValuePair(
           SL20Constants.PARAM_SL20_REQ_AUTH_VDA_LOCALE,
           language.toUpperCase(locale)));
diff --git a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20HttpBindingUtils.java b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20HttpBindingUtils.java
index 1d7c9646..d07c0e66 100644
--- a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20HttpBindingUtils.java
+++ b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20HttpBindingUtils.java
@@ -3,23 +3,129 @@ package at.gv.egiz.eaaf.modules.auth.sl20.utils;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.net.URISyntaxException;
+import java.text.MessageFormat;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.ParseException;
+import org.apache.http.StatusLine;
+import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.util.EntityUtils;
 import org.jose4j.base64url.Base64Url;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
+import at.gv.egiz.eaaf.modules.auth.sl20.exceptions.SlCommandoParserException;
+import lombok.Data;
+import lombok.Getter;
+
 public class SL20HttpBindingUtils {
   private static final Logger log = LoggerFactory.getLogger(SL20HttpBindingUtils.class);
 
+  private static JsonMapper mapper = new JsonMapper();
+  
+  @Data
+  @Getter
+  public static class Sl20ResponseHolder {
+    private final JsonNode responseBody;
+    private final StatusLine responseStatus;
+    private SlCommandoParserException error;
+
+  }
+  
+  /**
+   * Security-Layer 2.0 specific response-handler for Apache HTTP client.
+   * 
+   * @return {@link Sl20ResponseHolder}
+   */
+  public static ResponseHandler<Sl20ResponseHolder> sl20ResponseHandler() {
+    return response -> {
+      try {
+        final int httpStatusCode = response.getStatusLine().getStatusCode();
+        if (httpStatusCode == HttpStatus.OK.value()) {
+          if (response.getEntity().getContentType() == null) {
+            throw new SlCommandoParserException("SL20 response contains NO ContentType");
+            
+          }
+
+          if (!response.getEntity().getContentType().getValue().startsWith("application/json")) {
+            throw new SlCommandoParserException(
+                "SL20 response with a wrong ContentType: " + response.getEntity().getContentType().getValue());
+            
+          } 
+          
+          //parse OK response from body
+          return new Sl20ResponseHolder(parseSL20ResultFromResponse(response.getEntity()),
+              response.getStatusLine());                    
+        
+        } else if (httpStatusCode == HttpStatus.SEE_OTHER.value() 
+            || httpStatusCode == HttpStatus.TEMPORARY_REDIRECT.value()) {
+          final Header[] locationHeader = response.getHeaders("Location");
+          if (locationHeader == null) {
+            throw new SlCommandoParserException("Find Redirect statuscode but not Location header");
+            
+          }
+
+          final String sl20RespString = new URIBuilder(locationHeader[0].getValue()).getQueryParams().get(0).getValue();
+          return new Sl20ResponseHolder(mapper.getMapper().readTree(Base64Url.decode(sl20RespString)), 
+              response.getStatusLine()); 
+                             
+        } else if (
+            httpStatusCode == HttpStatus.INTERNAL_SERVER_ERROR.value() 
+            || httpStatusCode == HttpStatus.UNAUTHORIZED.value()
+            || httpStatusCode == HttpStatus.BAD_REQUEST.value()) {
+          log.info("SL20 response with http-code: {}. Search for error message", httpStatusCode);                    
+          
+          String bodyMsg = "_EMPTY_";
+          try {
+            //extract JSON body from defined http error-codes
+            bodyMsg = EntityUtils.toString(response.getEntity());            
+            log.info("SL20 response with http-code: {0} and errorMsg: {1}", httpStatusCode, bodyMsg);
+            Sl20ResponseHolder holder = new Sl20ResponseHolder(
+                mapper.getMapper().readTree(bodyMsg), response.getStatusLine());
+            return holder; 
+            
+          } catch (final IOException | ParseException e) {
+            log.warn("SL20 response contains no valid JSON", e);            
+            throw new SlCommandoParserException(MessageFormat.format(
+                "SL20 response with http-code: {0} with body: {1} and generic response-processing error: {2}", 
+                httpStatusCode, bodyMsg, e.getMessage()));
+            
+          }
+      
+        } else {
+          //all other HTTP StatusCodes
+          throw new SlCommandoParserException(MessageFormat.format(
+              "SL20 response with http-code: {0} and errorMsg: {1}", 
+              httpStatusCode, EntityUtils.toString(response.getEntity())));
+          
+        }
+      
+      } catch (SlCommandoParserException e) {
+        Sl20ResponseHolder holder = new Sl20ResponseHolder(null, response.getStatusLine());
+        holder.setError(e);
+        return holder;
+        
+      } catch (final Exception e) {        
+        Sl20ResponseHolder holder = new Sl20ResponseHolder(null, response.getStatusLine());
+        holder.setError(
+            new SlCommandoParserException("SL20 response parsing FAILED! Reason: " + e.getMessage(), e));
+        return holder;
+        
+      }
+    };
+  }
+    
   /**
    * Write SL2.0 response into http-response object
    *
@@ -59,6 +165,24 @@ public class SL20HttpBindingUtils {
       httpResp.setHeader("Location", clientRedirectUri.build().toString());
 
     }
+  }
+  
+  private static JsonNode parseSL20ResultFromResponse(final HttpEntity resp) throws Exception {
+    if (resp != null && resp.getContent() != null) {
+      final String rawSL20Resp = EntityUtils.toString(resp);
+      final JsonNode sl20Resp = mapper.getMapper().readTree(rawSL20Resp);
+
+      // TODO: check sl20Resp type like && sl20Resp.isJsonObject()
+      if (sl20Resp != null) {
+        return sl20Resp;
+
+      } else {
+        throw new SlCommandoParserException("SL2.0 can NOT parse to a JSON object");
+      }
+
+    } else {
+      throw new SlCommandoParserException("Can NOT find content in http response");
+    }
 
   }
 }
diff --git a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20JsonExtractorUtils.java b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20JsonExtractorUtils.java
index 40ea0430..bed25c0c 100644
--- a/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20JsonExtractorUtils.java
+++ b/eaaf_modules/eaaf_module_auth_sl20/src/main/java/at/gv/egiz/eaaf/modules/auth/sl20/utils/SL20JsonExtractorUtils.java
@@ -8,12 +8,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.util.EntityUtils;
-import org.jose4j.base64url.Base64Url;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -299,84 +293,6 @@ public class SL20JsonExtractorUtils {
 
   }
 
-  /**
-   * Extract generic transport container from httpResponse.
-   *
-   * @param httpResp Http response object
-   * @return JSON with SL2.0 response
-   * @throws SlCommandoParserException In case of an error
-   */
-  public static JsonNode getSL20ContainerFromResponse(final HttpResponse httpResp) throws SlCommandoParserException {
-    try {
-      JsonNode sl20Resp = null;
-      if (httpResp.getStatusLine().getStatusCode() == 303 || httpResp.getStatusLine().getStatusCode() == 307) {
-        final Header[] locationHeader = httpResp.getHeaders("Location");
-        if (locationHeader == null) {
-          throw new SlCommandoParserException("Find Redirect statuscode but not Location header");
-        }
-
-        final String sl20RespString = new URIBuilder(locationHeader[0].getValue()).getQueryParams().get(0).getValue();
-        sl20Resp = mapper.getMapper().readTree(Base64Url.decode(sl20RespString));
-
-      } else if (httpResp.getStatusLine().getStatusCode() == 200) {
-        if (httpResp.getEntity().getContentType() == null) {
-          throw new SlCommandoParserException("SL20 response contains NO ContentType");
-        }
-
-        if (!httpResp.getEntity().getContentType().getValue().startsWith("application/json")) {
-          throw new SlCommandoParserException(
-              "SL20 response with a wrong ContentType: " + httpResp.getEntity().getContentType().getValue());
-        }
-        sl20Resp = parseSL20ResultFromResponse(httpResp.getEntity());
-
-      } else if (httpResp.getStatusLine().getStatusCode() == 500 || httpResp.getStatusLine().getStatusCode() == 401
-          || httpResp.getStatusLine().getStatusCode() == 400) {
-        log.info(
-            "SL20 response with http-code: " + httpResp.getStatusLine().getStatusCode() + ". Search for error message");
-
-        try {
-          sl20Resp = parseSL20ResultFromResponse(httpResp.getEntity());
-
-        } catch (final Exception e) {
-          log.warn("SL20 response contains no valid JSON", e);
-          throw new SlCommandoParserException("SL20 response with http-code: "
-              + httpResp.getStatusLine().getStatusCode() + " AND NO valid JSON errormsg", e);
-
-        }
-
-      } else {
-        throw new SlCommandoParserException(
-            "SL20 response with http-code: " + httpResp.getStatusLine().getStatusCode());
-      }
-
-      log.info("Find JSON object in http response");
-      return sl20Resp;
-
-    } catch (final Exception e) {
-      throw new SlCommandoParserException("SL20 response parsing FAILED! Reason: " + e.getMessage(), e);
-
-    }
-  }
-
-  private static JsonNode parseSL20ResultFromResponse(final HttpEntity resp) throws Exception {
-    if (resp != null && resp.getContent() != null) {
-      final String rawSL20Resp = EntityUtils.toString(resp);
-      final JsonNode sl20Resp = mapper.getMapper().readTree(rawSL20Resp);
-
-      // TODO: check sl20Resp type like && sl20Resp.isJsonObject()
-      if (sl20Resp != null) {
-        return sl20Resp;
-
-      } else {
-        throw new SlCommandoParserException("SL2.0 can NOT parse to a JSON object");
-      }
-
-    } else {
-      throw new SlCommandoParserException("Can NOT find content in http response");
-    }
-
-  }
-
   private static JsonNode getAndCheck(final JsonNode input, final String keyID, final boolean isRequired)
       throws SlCommandoParserException {
     final JsonNode internal = input.get(keyID);
-- 
cgit v1.2.3