AbstractApiValidator.java

  1. /*
  2.  * GovWay - A customizable API Gateway
  3.  * https://govway.org
  4.  *
  5.  * Copyright (c) 2005-2025 Link.it srl (https://link.it).
  6.  *
  7.  * This program is free software: you can redistribute it and/or modify
  8.  * it under the terms of the GNU General Public License version 3, as published by
  9.  * the Free Software Foundation.
  10.  *
  11.  * This program is distributed in the hope that it will be useful,
  12.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  14.  * GNU General Public License for more details.
  15.  *
  16.  * You should have received a copy of the GNU General Public License
  17.  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  18.  *
  19.  */

  20. package org.openspcoop2.utils.rest;

  21. import java.net.URL;
  22. import java.util.ArrayList;
  23. import java.util.List;

  24. import org.openspcoop2.utils.UtilsMultiException;
  25. import org.openspcoop2.utils.rest.api.Api;
  26. import org.openspcoop2.utils.rest.api.ApiBodyParameter;
  27. import org.openspcoop2.utils.rest.api.ApiCookieParameter;
  28. import org.openspcoop2.utils.rest.api.ApiHeaderParameter;
  29. import org.openspcoop2.utils.rest.api.ApiOperation;
  30. import org.openspcoop2.utils.rest.api.ApiParameterSchema;
  31. import org.openspcoop2.utils.rest.api.ApiParameterSchemaComplexType;
  32. import org.openspcoop2.utils.rest.api.ApiParameterTypeSchema;
  33. import org.openspcoop2.utils.rest.api.ApiRequestDynamicPathParameter;
  34. import org.openspcoop2.utils.rest.api.ApiRequestFormParameter;
  35. import org.openspcoop2.utils.rest.api.ApiRequestQueryParameter;
  36. import org.openspcoop2.utils.rest.api.ApiResponse;
  37. import org.openspcoop2.utils.rest.api.ApiSchemaTypeRestriction;
  38. import org.openspcoop2.utils.rest.api.ApiUtilities;
  39. import org.openspcoop2.utils.rest.entity.Cookie;
  40. import org.openspcoop2.utils.rest.entity.HttpBaseEntity;
  41. import org.openspcoop2.utils.rest.entity.HttpBaseRequestEntity;
  42. import org.openspcoop2.utils.rest.entity.HttpBaseResponseEntity;
  43. import org.openspcoop2.utils.transport.TransportUtils;
  44. import org.openspcoop2.utils.transport.http.ContentTypeUtilities;
  45. import org.springframework.web.util.UriUtils;

  46. /**
  47.  * ApiValidatorConfig
  48.  *
  49.  *
  50.  * @author Poli Andrea (apoli@link.it)
  51.  * @author $Author$
  52.  * @version $Rev$, $Date$
  53.  */
  54. public abstract class AbstractApiValidator   {

  55.     public abstract void validatePreConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation,Object ... args) throws ProcessingException,ValidatorException;
  56.    
  57.     public abstract void validatePostConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation,Object ... args) throws ProcessingException,ValidatorException;
  58.    
  59.     public abstract void validateValueAsType(ApiParameterType parameterType, String value, String type, ApiSchemaTypeRestriction typeRestriction) throws ProcessingException,ValidatorException;
  60.    
  61.     public void validate(Api api, HttpBaseEntity<?> httpEntity, Object ... args) throws ProcessingException,ValidatorException{
  62.        
  63.         ApiOperation operation = api.findOperation(httpEntity.getMethod(), httpEntity.getUrl());
  64.         if(operation==null){
  65.             throw new ProcessingException("Resource "+httpEntity.getMethod()+" '"+httpEntity.getUrl()+"' not found");
  66.         }

  67.         // es. xsd
  68.         validatePreConformanceCheck(httpEntity, operation, args);
  69.        
  70.         validateConformanceCheck(httpEntity, operation, api.getBaseURL());
  71.        
  72.         // es. elementi specifici come nomi nel xsd etc..
  73.         validatePostConformanceCheck(httpEntity, operation, args);
  74.        
  75.     }
  76.    
  77.     private void validateConformanceCheck(HttpBaseEntity<?> httpEntity,ApiOperation operation, URL baseUrl) throws ProcessingException,ValidatorException{

  78.         try{
  79.            
  80.            
  81.            
  82.             if(httpEntity.getContentType() != null) {
  83.                
  84.                 String baseTypeHttp = ContentTypeUtilities.readBaseTypeFromContentType(httpEntity.getContentType());
  85.                
  86.                 boolean contentTypeSupported = false;
  87.                 List<ApiBodyParameter> requestBodyParametersList = null;
  88.                 List<ApiResponse> responses = null;
  89.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeBodyParameters()>0){
  90.                     requestBodyParametersList = operation.getRequest().getBodyParameters();
  91.                 }
  92.                 if(operation.sizeResponses()>0){
  93.                     responses = operation.getResponses();
  94.                 }
  95.                 int status = -1;
  96.                
  97.                 if(httpEntity instanceof HttpBaseRequestEntity<?>) {
  98.                                        
  99.                     if(requestBodyParametersList != null) {
  100.                         for(ApiBodyParameter input: requestBodyParametersList) {
  101.                             if(input.isAllMediaType() || ContentTypeUtilities.isMatch(null, baseTypeHttp, input.getMediaType())) {
  102.                                 contentTypeSupported = true;
  103.                                 break;
  104.                             }
  105.                         }
  106.                     }
  107.                 } else if(httpEntity instanceof HttpBaseResponseEntity<?>) {
  108.                     status = ((HttpBaseResponseEntity<?>)httpEntity).getStatus();
  109.                    
  110.                     if(responses != null) {
  111.                        
  112.                         // Fix: se si traccia del codice http esatto non devo andare a verificare il codice di default
  113.                        
  114.                         // prima faccio la verifica con codice esatto
  115.                         ApiResponse outputDefault = null;
  116.                         boolean findExactResponseCode = false;
  117.                         for(ApiResponse output: responses) {
  118.                             if(status==output.getHttpReturnCode()) {
  119.                                 findExactResponseCode = true;
  120.                                 if(output.sizeBodyParameters()>0) {
  121.                                     for(ApiBodyParameter outputBodyParameter: output.getBodyParameters()) {
  122.                                         if(outputBodyParameter.isAllMediaType() || ContentTypeUtilities.isMatch(null, baseTypeHttp, outputBodyParameter.getMediaType()) ) {
  123.                                             contentTypeSupported = true;
  124.                                             break;
  125.                                         }
  126.                                     }
  127.                                 }
  128.                             }
  129.                             else if(output.isDefaultHttpReturnCode()) {
  130.                                 outputDefault = output;
  131.                             }
  132.                         }
  133.                        
  134.                         // poi con l'eventuale default
  135.                         if(!contentTypeSupported && !findExactResponseCode && outputDefault!=null) {
  136.                             if(outputDefault.sizeBodyParameters()>0) {
  137.                                 for(ApiBodyParameter outputBodyParameter: outputDefault.getBodyParameters()) {
  138.                                     if(outputBodyParameter.isAllMediaType() || ContentTypeUtilities.isMatch(null, baseTypeHttp, outputBodyParameter.getMediaType()) ) {
  139.                                         contentTypeSupported = true;
  140.                                         break;
  141.                                     }
  142.                                 }
  143.                             }
  144.                         }
  145.                     }
  146.    
  147.                 }
  148.                
  149.                 if(!contentTypeSupported) {
  150.                     if(status>0)
  151.                         throw new ValidatorException("Content-Type '"+baseTypeHttp+"' (http response status '"+status+"') unsupported");
  152.                     else
  153.                         throw new ValidatorException("Content-Type '"+baseTypeHttp+"' unsupported");
  154.                 }
  155.             }
  156.             else {
  157.                
  158.                 // senza content-type
  159.                
  160.                 if(httpEntity instanceof HttpBaseRequestEntity<?>) {
  161.                    
  162.                     if(operation.getRequest()!=null &&  operation.getRequest().sizeBodyParameters()>0){
  163.                        
  164.                         boolean required = false;
  165.                         for(ApiBodyParameter input: operation.getRequest().getBodyParameters()) {
  166.                             if(input.isRequired()) {
  167.                                 required = true;
  168.                             }
  169.                         }
  170.                         if(required) {
  171.                             throw new ValidatorException("Request without payload (Content-Type 'null') unsupported");
  172.                         }
  173.                        
  174.                     }
  175.                    
  176.                 }
  177.                
  178.                
  179.             }
  180.            
  181.             if(httpEntity instanceof HttpBaseRequestEntity<?>) {
  182.            
  183.                 HttpBaseRequestEntity<?> request = (HttpBaseRequestEntity<?>) httpEntity;
  184.                
  185.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeHeaderParameters()>0){
  186.                     for (ApiHeaderParameter paramHeader : operation.getRequest().getHeaderParameters()) {
  187.                         String name = paramHeader.getName();
  188.                         List<String> values = TransportUtils.getRawObject(request.getHeaders(), name);
  189.                         if(values==null || values.isEmpty()){
  190.                             if(paramHeader.isRequired()){
  191.                                 throw new ValidatorException("Required http header '"+name+"' not found");
  192.                             }
  193.                         }
  194.                         else {
  195.                             for (String value : values) {
  196.                                 if(value!=null){
  197.                                     validate(paramHeader.getApiParameterSchema(), ApiParameterType.header, name, value, "http header");
  198.                                 }  
  199.                             }
  200.                         }
  201.                     }
  202.                 }
  203.                
  204.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeCookieParameters()>0){
  205.                     for (ApiCookieParameter paramCookie : operation.getRequest().getCookieParameters()) {
  206.                         String name = paramCookie.getName();
  207.                         String value = null;
  208.                         if(request.getCookies()!=null){
  209.                             for (Cookie cookie : request.getCookies()) {
  210.                                 if(name.equalsIgnoreCase(cookie.getName())){
  211.                                     value = cookie.getValue();
  212.                                 }
  213.                             }
  214.                         }
  215.                         if(value==null){
  216.                             if(paramCookie.isRequired()){
  217.                                 throw new ValidatorException("Required Cookie '"+name+"' not found");
  218.                             }
  219.                         }
  220.                         if(value!=null){
  221.                             validate(paramCookie.getApiParameterSchema(), ApiParameterType.cookie, name, value, "cookie");
  222.                         }
  223.                     }
  224.                 }
  225.                
  226.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeQueryParameters()>0){
  227.                     for (ApiRequestQueryParameter paramQuery : operation.getRequest().getQueryParameters()) {
  228.                        
  229.                         // i parametri sono esplosi. La validazione viene fatta con json o openapi  
  230.                         boolean parametriEsplosi = false;
  231.                         if(paramQuery.getApiParameterSchema()!=null && paramQuery.getApiParameterSchema().getSchemas()!=null && !paramQuery.getApiParameterSchema().getSchemas().isEmpty()) {
  232.                             for (ApiParameterTypeSchema schema : paramQuery.getApiParameterSchema().getSchemas()) {
  233.                                 // Il controllo basta farlo sul primo.
  234.                                 if(schema.getSchema()!=null && schema.getSchema().isTypeObject()) {
  235.                                     if (
  236.                                             (schema.getSchema().isStyleQueryForm() && schema.getSchema().isExplodeEnabled())
  237.                                             ||
  238.                                             (schema.getSchema().isStyleQueryDeepObject())
  239.                                             ) {
  240.                                         parametriEsplosi = true;
  241.                                         break;
  242.                                     }
  243.                                 }
  244.                             }
  245.                         }
  246.                         if(parametriEsplosi) {
  247.                             continue;
  248.                         }
  249.                        
  250.                         String name = paramQuery.getName();
  251.                         List<String> values = TransportUtils.getRawObject(request.getParameters(), name);
  252.                         if(values==null || values.isEmpty()){
  253.                             if(paramQuery.isRequired()){
  254.                                 throw new ValidatorException("Required query parameter '"+name+"' not found");
  255.                             }
  256.                         }
  257.                         else {
  258.                             for (String value : values) {
  259.                                 if(value!=null){
  260.                                     validate(paramQuery.getApiParameterSchema(), ApiParameterType.query, name, value, "query parameter");
  261.                                 }
  262.                             }
  263.                         }
  264.                     }
  265.                 }
  266.                                
  267.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeDynamicPathParameters()>0){
  268.                     for (ApiRequestDynamicPathParameter paramDynamicPath : operation.getRequest().getDynamicPathParameters()) {
  269.                         boolean find = false;
  270.                         String valueFound = null;
  271.                         if(operation.isDynamicPath()){
  272.                             for (int i = 0; i < operation.sizePath(); i++) {
  273.                                 if(operation.isDynamicPath()){
  274.                                     String idDinamic = operation.getDynamicPathId(i);
  275.                                     if(paramDynamicPath.getName().equals(idDinamic)){
  276.                                         String [] urlList = ApiUtilities.extractUrlList(baseUrl, request.getUrl());
  277.                                         if(i>=urlList.length){
  278.                                             throw new ValidatorException("Dynamic path '"+paramDynamicPath.getName()+"' not found (position:"+i+", urlLenght:"+urlList.length+")");
  279.                                         }
  280.                                         find = true;
  281.                                         valueFound = urlList[i];
  282.                                         break;
  283.                                     }
  284.                                 }
  285.                             }
  286.                         }
  287.                         if(!find && paramDynamicPath.isRequired()){
  288.                             throw new ValidatorException("Required dynamic path '"+paramDynamicPath.getName()+"' not found");
  289.                         }
  290.                         if(find){
  291.                             String valueUrlDecoded = valueFound;
  292.                             // il valore può essere url encoded
  293.                             try {
  294.                                 // Note that Java’s URLEncoder class encodes space character(" ") into a + sign.
  295.                                 // This is contrary to other languages like Javascript that encode space character into %20.
  296.                                 /*
  297.                                  *  URLEncoder is not for encoding URLs, but for encoding parameter names and values for use in GET-style URLs or POST forms.
  298.                                  *  That is, for transforming plain text into the application/x-www-form-urlencoded MIME format as described in the HTML specification.
  299.                                  **/
  300.                                 //valueUrlDecoded = java.net.URLDecoder.decode(valueFound, org.openspcoop2.utils.resources.Charset.UTF_8.getValue());
  301.                                
  302.                                 valueUrlDecoded = UriUtils.decode(valueFound, org.openspcoop2.utils.resources.Charset.UTF_8.getValue());
  303.                                
  304.                                 //System.out.println("DOPO '"+valueUrlDecoded+"' PRIMA '"+valueFound+"'");
  305.                                
  306.                             }catch(Throwable e) {
  307.                                 //System.out.println("ERRORE");
  308.                                 //e.printStackTrace(System.out);
  309.                                 // utilizzo valore originale
  310.                                 //throw new RuntimeException(e.getMessage(),e);
  311.                             }
  312.                             validate(paramDynamicPath.getApiParameterSchema(), ApiParameterType.path, paramDynamicPath.getName(), valueUrlDecoded, "dynamic path",
  313.                                     (valueUrlDecoded!=null && !valueUrlDecoded.equals(valueFound)) ? valueFound : null);
  314.                         }
  315.                     }
  316.                 }
  317.                
  318.                 if(operation.getRequest()!=null &&  operation.getRequest().sizeFormParameters()>0){
  319.                     for (ApiRequestFormParameter paramForm : operation.getRequest().getFormParameters()) {
  320.                         String name = paramForm.getName();
  321.                         List<String> values = TransportUtils.getRawObject(request.getParameters(), name);
  322.                         if(values==null || values.isEmpty()){
  323.                             if(paramForm.isRequired()){
  324.                                 throw new ValidatorException("Required form parameter '"+name+"' not found");
  325.                             }
  326.                         }
  327.                         else {
  328.                             for (String value : values) {
  329.                                 if(value!=null){
  330.                                     validate(paramForm.getApiParameterSchema(), ApiParameterType.form, name, value, "form parameter");
  331.                                 }
  332.                             }
  333.                         }
  334.                     }
  335.                 }
  336.                
  337.             }
  338.            
  339.             if(httpEntity instanceof HttpBaseResponseEntity<?>) {
  340.                
  341.                 HttpBaseResponseEntity<?> response = (HttpBaseResponseEntity<?>) httpEntity;
  342.                 ApiResponse apiResponseFound = null;
  343.                 ApiResponse apiResponseDefault = null;
  344.                
  345.                 for (ApiResponse apiResponse : operation.getResponses()) {
  346.                     if(apiResponse.isDefaultHttpReturnCode()) {
  347.                         apiResponseDefault = apiResponse;
  348.                     }
  349.                     if(response.getStatus() == apiResponse.getHttpReturnCode()){
  350.                         apiResponseFound = apiResponse;
  351.                         break;
  352.                     }                                      
  353.                 }
  354.                
  355.                 if(apiResponseFound==null && apiResponseDefault!=null) {
  356.                     apiResponseFound = apiResponseDefault;
  357.                 }
  358.                 if(apiResponseFound==null){
  359.                     throw new ValidatorException("Http status code '"+response.getStatus()+"' unsupported");
  360.                 }
  361.                
  362.                 if(apiResponseFound.sizeHeaderParameters()>0){
  363.                     for (ApiHeaderParameter paramHeader : apiResponseFound.getHeaderParameters()) {
  364.                         String name = paramHeader.getName();
  365.                         List<String> values = TransportUtils.getRawObject(response.getHeaders(), name);
  366.                         if(values==null || values.isEmpty()){
  367.                             if(paramHeader.isRequired()){
  368.                                 throw new ValidatorException("Required http header '"+name+"' not found");
  369.                             }
  370.                         }
  371.                         else {
  372.                             for (String value : values) {
  373.                                 if(value!=null){
  374.                                     validate(paramHeader.getApiParameterSchema(), ApiParameterType.header, name, value, "http header");
  375.                                 }
  376.                             }
  377.                         }
  378.                     }
  379.                 }
  380.                
  381.                 if(apiResponseFound.sizeCookieParameters()>0){
  382.                     for (ApiCookieParameter paramCookie : apiResponseFound.getCookieParameters()) {
  383.                         String name = paramCookie.getName();
  384.                         String value = null;
  385.                         if(response.getCookies()!=null){
  386.                             for (Cookie cookie : response.getCookies()) {
  387.                                 if(name.equalsIgnoreCase(cookie.getName())){
  388.                                     value = cookie.getValue();
  389.                                 }
  390.                             }
  391.                         }
  392.                         if(value==null){
  393.                             if(paramCookie.isRequired()){
  394.                                 throw new ValidatorException("Required cookie '"+name+"' not found");
  395.                             }
  396.                         }
  397.                         if(value!=null){
  398.                             validate(paramCookie.getApiParameterSchema(), ApiParameterType.cookie, name, value, "cookie");
  399.                         }
  400.                     }
  401.                 }
  402.                
  403.             }

  404.         }catch(ProcessingException e){
  405.             throw e;
  406.         }catch(ValidatorException e){
  407.             throw e;
  408.         }catch(Exception e){
  409.             throw new ProcessingException(e.getMessage(),e);
  410.         }
  411.        
  412.     }
  413.    
  414.    
  415.     private void validate(ApiParameterSchema apiParameterSchema, ApiParameterType type, String name, String value, String position) throws ValidatorException, ProcessingException {
  416.         this.validate(apiParameterSchema, type, name, value, position, null);
  417.     }
  418.     private void validate(ApiParameterSchema apiParameterSchema, ApiParameterType type, String name, String value, String position, String valueUrlEncoded) throws ValidatorException, ProcessingException {
  419.         if(apiParameterSchema!=null && apiParameterSchema.getSchemas()!=null && !apiParameterSchema.getSchemas().isEmpty()) {
  420.             ApiParameterSchemaComplexType ct = apiParameterSchema.getComplexType();
  421.             if(ct==null) {
  422.                 ct = ApiParameterSchemaComplexType.simple;
  423.             }
  424.            
  425.             String prefixError = "Invalid value '"+value+"' in "+position+" '"+name+"'";
  426.             if(valueUrlEncoded!=null) {
  427.                  prefixError = "Invalid value '"+value+"' (urlEncoded: '"+valueUrlEncoded+"') in "+position+" '"+name+"'";
  428.             }
  429.            
  430.             switch (ct) {
  431.             case simple: // caso speciale con 1 solo
  432.             case allOf:
  433.                 StringBuilder sbException = new StringBuilder();
  434.                 List<ValidatorException> listValidatorException = new ArrayList<ValidatorException>();
  435.                 for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
  436.                     try{
  437.                         validateValueAsType(type, value, schema.getType(),schema.getSchema());
  438.                     }catch(ValidatorException val){
  439.                         String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
  440.                         if(sbException.length()>0) {
  441.                             sbException.append("\n");
  442.                         }
  443.                         sbException.append(msgError);
  444.                         listValidatorException.add( new ValidatorException(msgError,val) );
  445.                     }  
  446.                 }
  447.                 if(!listValidatorException.isEmpty()) {
  448.                     UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
  449.                     throw new ValidatorException(sbException.toString(), multi);
  450.                 }
  451.                 return;
  452.                
  453.             case anyOf:
  454.                 sbException = new StringBuilder();
  455.                 listValidatorException = new ArrayList<ValidatorException>();
  456.                 for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
  457.                     try{
  458.                         validateValueAsType(type, value, schema.getType(),schema.getSchema());
  459.                         return; // ne basta una che valida correttamente
  460.                     }catch(ValidatorException val){
  461.                         String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
  462.                         if(sbException.length()>0) {
  463.                             sbException.append("\n");
  464.                         }
  465.                         sbException.append(msgError);
  466.                         listValidatorException.add( new ValidatorException(msgError,val) );
  467.                     }  
  468.                 }
  469.                 if(!listValidatorException.isEmpty()) {
  470.                     UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
  471.                     throw new ValidatorException(sbException.toString(), multi);
  472.                 }
  473.                 return; //caso che non può accadere
  474.                
  475.             case oneOf:
  476.                 sbException = new StringBuilder();
  477.                 listValidatorException = new ArrayList<ValidatorException>();
  478.                 int schemiValidi = 0;
  479.                 for (ApiParameterTypeSchema schema : apiParameterSchema.getSchemas()) {
  480.                     try{
  481.                         validateValueAsType(type, value, schema.getType(),schema.getSchema());
  482.                         schemiValidi++;
  483.                     }catch(ValidatorException val){
  484.                         String msgError = prefixError+" (expected type '"+schema.getType()+"'): "+val.getMessage();
  485.                         if(sbException.length()>0) {
  486.                             sbException.append("\n");
  487.                         }
  488.                         sbException.append(msgError);
  489.                         listValidatorException.add( new ValidatorException(msgError,val) );
  490.                     }  
  491.                 }
  492.                 if(schemiValidi==0) {
  493.                     if(!listValidatorException.isEmpty()) {
  494.                         UtilsMultiException multi = new UtilsMultiException(listValidatorException.toArray(new ValidatorException[1]));
  495.                         throw new ValidatorException(sbException.toString(), multi);
  496.                     }
  497.                     else {
  498.                         throw new ValidatorException(prefixError);
  499.                     }
  500.                 }
  501.                 else if(schemiValidi>1) {
  502.                     throw new ValidatorException(prefixError+": expected validates the value against exactly one of the subschemas; founded valid in "+schemiValidi+" schemas");
  503.                 }
  504.                 return; // deve essere validato rispetto esattamente ad uno schema

  505.             }
  506.         }

  507.        

  508.     }
  509. }