package io.swagger.handler; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.fge.jsonschema.core.report.ListProcessingReport; import com.github.fge.jsonschema.core.report.LogLevel; import com.github.fge.jsonschema.core.report.ProcessingMessage; import com.github.fge.jsonschema.core.report.ProcessingReport; import com.github.fge.jsonschema.main.JsonSchema; import com.github.fge.jsonschema.main.JsonSchemaFactory; import io.swagger.oas.inflector.models.RequestContext; import io.swagger.oas.inflector.models.ResponseContext; import io.swagger.parser.SwaggerParser; import io.swagger.v3.parser.core.models.SwaggerParseResult; import io.swagger.parser.util.SwaggerDeserializationResult; import io.swagger.v3.parser.OpenAPIV3Parser; import io.swagger.util.Json; import io.swagger.util.Yaml; import io.swagger.models.SchemaValidationError; import io.swagger.models.ValidationResponse; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.StatusLine; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.TrustStrategy; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.core.Response; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class ValidatorController{ static final String SCHEMA_FILE = "schema3.json"; static final String SCHEMA_URL = "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json"; static final String SCHEMA2_FILE = "schema.json"; static final String SCHEMA2_URL = "http://swagger.io/v2/schema.json"; static final String INVALID_VERSION = "Deprecated Swagger version. Please visit http://swagger.io for information on upgrading to Swagger/OpenAPI 2.0 or OpenAPI 3.0"; static Logger LOGGER = LoggerFactory.getLogger(ValidatorController.class); static long LAST_FETCH = 0; static long LAST_FETCH_V3 = 0; static ObjectMapper JsonMapper = Json.mapper(); static ObjectMapper YamlMapper = Yaml.mapper(); private JsonSchema schemaV2; private JsonSchema schemaV3; static boolean rejectLocal = StringUtils.isBlank(System.getProperty("rejectLocal")) ? true : Boolean.parseBoolean(System.getProperty("rejectLocal")); static boolean rejectRedirect = StringUtils.isBlank(System.getProperty("rejectRedirect")) ? true : Boolean.parseBoolean(System.getProperty("rejectRedirect")); public ResponseContext validateByUrl(RequestContext request , String url) { if(url == null) { return new ResponseContext() .status(Response.Status.BAD_REQUEST) .entity( "No specification supplied in either the url or request body. Try again?" ); } ValidationResponse validationResponse = null; try { validationResponse = debugByUrl(request, url); }catch (Exception e){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process URL" ); } return processValidationResponse(validationResponse); } public ResponseContext validateByContent(RequestContext request, JsonNode inputSpec) { if(inputSpec == null) { return new ResponseContext() .status(Response.Status.BAD_REQUEST) .entity( "No specification supplied in either the url or request body. Try again?" ); } String inputAsString = Json.pretty(inputSpec); ValidationResponse validationResponse = null; try { validationResponse = debugByContent(request ,inputAsString); }catch (Exception e){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process URL" ); } return processValidationResponse(validationResponse); } private ResponseContext processValidationResponse(ValidationResponse validationResponse) { if (validationResponse == null){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process specification" ); } boolean valid = true; boolean upgrade = false; List messages = new ArrayList<>(); if (validationResponse.getMessages() != null) { for (String message : validationResponse.getMessages()) { if (message != null) { messages.add(message); if(message.endsWith("is unsupported")) { valid = true; }else{ valid = false; } } } } if (validationResponse.getSchemaValidationMessages() != null) { for (SchemaValidationError error : validationResponse.getSchemaValidationMessages()) { if (error != null) { messages.add(error.getMessage()); if (error.getLevel() != null && error.getLevel().toLowerCase().contains("error")) { valid= false; } if (INVALID_VERSION.equals(error.getMessage())) { upgrade = true; } } } } if (upgrade == true ){ return new ResponseContext() .contentType("image/png") .entity(this.getClass().getClassLoader().getResourceAsStream("upgrade.png")); }else if (valid == true ){ return new ResponseContext() .contentType("image/png") .entity(this.getClass().getClassLoader().getResourceAsStream("valid.png")); } else{ return new ResponseContext() .contentType("image/png") .entity(this.getClass().getClassLoader().getResourceAsStream("invalid.png")); } } public ResponseContext reviewByUrl(RequestContext request , String url) { if(url == null) { return new ResponseContext() .status(Response.Status.BAD_REQUEST) .entity( "No specification supplied in either the url or request body. Try again?" ); } ValidationResponse validationResponse = null; try { validationResponse = debugByUrl(request, url); }catch (Exception e){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process specification" ); } return new ResponseContext() .entity(validationResponse); //return processDebugValidationResponse(validationResponse); } public ResponseContext reviewByContent(RequestContext request, JsonNode inputSpec) { if(inputSpec == null) { return new ResponseContext() .status(Response.Status.BAD_REQUEST) .entity( "No specification supplied in either the url or request body. Try again?" ); } String inputAsString = Json.pretty(inputSpec); ValidationResponse validationResponse = null; try { validationResponse = debugByContent(request ,inputAsString); }catch (Exception e){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process specification" ); } return new ResponseContext() .entity(validationResponse); //return processDebugValidationResponse(validationResponse); } private ResponseContext processDebugValidationResponse(ValidationResponse validationResponse) { if (validationResponse == null){ return new ResponseContext().status(Response.Status.INTERNAL_SERVER_ERROR).entity( "Failed to process specification" ); } List messages = new ArrayList<>(); if (validationResponse.getMessages() != null) { for (String message : validationResponse.getMessages()) { if (message != null) { messages.add(message); } } } if (validationResponse.getSchemaValidationMessages() != null) { for (SchemaValidationError error : validationResponse.getSchemaValidationMessages()) { if (error != null) { messages.add(error.getMessage()); } } } return new ResponseContext() .entity(messages); } public ValidationResponse debugByUrl( RequestContext request, String url) throws Exception { ValidationResponse output = new ValidationResponse(); String content; if(StringUtils.isBlank(url)) { ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage("No valid URL specified"); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } // read the spec contents, bail if it fails try { content = getUrlContents(url, ValidatorController.rejectLocal, ValidatorController.rejectRedirect); } catch (Exception e) { ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage("Can't read from file " + url); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } return debugByContent(request, content); } public ValidationResponse debugByContent(RequestContext request, String content) throws Exception { ValidationResponse output = new ValidationResponse(); // convert to a JsonNode JsonNode spec = readNode(content); if (spec == null) { ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage("Unable to read content. It may be invalid JSON or YAML"); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } boolean isVersion2 = false; // get the version, return deprecated if version 1.x String version = getVersion(spec); if (version != null && (version.startsWith("\"1") || version.startsWith("1"))) { ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage(INVALID_VERSION); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } else if (version != null && (version.startsWith("\"2") || version.startsWith("2"))) { isVersion2 = true; SwaggerDeserializationResult result = null; try { result = readSwagger(content); } catch (Exception e) { LOGGER.debug("can't read Swagger contents", e); ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage("unable to parse Swagger: " + e.getMessage()); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } if (result != null) { for (String message : result.getMessages()) { output.addMessage(message); } } } else if (version == null || (version.startsWith("\"3") || version.startsWith("3"))) { SwaggerParseResult result = null; try { result = readOpenApi(content); } catch (Exception e) { LOGGER.debug("can't read OpenAPI contents", e); ProcessingMessage pm = new ProcessingMessage(); pm.setLogLevel(LogLevel.ERROR); pm.setMessage("unable to parse OpenAPI: " + e.getMessage()); output.addValidationMessage(new SchemaValidationError(pm.asJson())); return output; } if (result != null) { for (String message : result.getMessages()) { output.addMessage(message); } } } // do actual JSON schema validation JsonSchema schema = getSchema(isVersion2); ProcessingReport report = schema.validate(spec); ListProcessingReport lp = new ListProcessingReport(); lp.mergeWith(report); java.util.Iterator<ProcessingMessage> it = lp.iterator(); while (it.hasNext()) { ProcessingMessage pm = it.next(); output.addValidationMessage(new SchemaValidationError(pm.asJson())); } return output; } private JsonSchema getSchema(boolean isVersion2) throws Exception { if (isVersion2) { return getSchemaV2(); } else { return getSchemaV3(); } } private JsonSchema getSchemaV3() throws Exception { if (schemaV3 != null && (System.currentTimeMillis() - LAST_FETCH_V3) < 600000) { return schemaV3; } try { LOGGER.debug("returning online schema v3"); LAST_FETCH_V3 = System.currentTimeMillis(); schemaV3 = resolveJsonSchema(getUrlContents(SCHEMA_URL), true); return schemaV3; } catch (Exception e) { LOGGER.warn("error fetching schema v3 from GitHub, using local copy"); schemaV3 = resolveJsonSchema(getResourceFileAsString(SCHEMA_FILE), true); LAST_FETCH_V3 = System.currentTimeMillis(); return schemaV3; } } private JsonSchema getSchemaV2() throws Exception { if (schemaV2 != null && (System.currentTimeMillis() - LAST_FETCH) < 600000) { return schemaV2; } try { LOGGER.debug("returning online schema"); LAST_FETCH = System.currentTimeMillis(); schemaV2 = resolveJsonSchema(getUrlContents(SCHEMA2_URL)); return schemaV2; } catch (Exception e) { LOGGER.warn("error fetching schema from GitHub, using local copy"); schemaV2 = resolveJsonSchema(getResourceFileAsString(SCHEMA2_FILE)); LAST_FETCH = System.currentTimeMillis(); return schemaV2; } } private JsonSchema resolveJsonSchema(String schemaAsString) throws Exception { return resolveJsonSchema(schemaAsString, false); } private JsonSchema resolveJsonSchema(String schemaAsString, boolean removeId) throws Exception { JsonNode schemaObject = JsonMapper.readTree(schemaAsString); if (removeId) { ObjectNode oNode = (ObjectNode) schemaObject; if (oNode.get("id") != null) { oNode.remove("id"); } if (oNode.get("$schema") != null) { oNode.remove("$schema"); } if (oNode.get("description") != null) { oNode.remove("description"); } } JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); return factory.getJsonSchema(schemaObject); } private CloseableHttpClient getCarelessHttpClient(boolean disableRedirect) { CloseableHttpClient httpClient = null; try { SSLContextBuilder builder = new SSLContextBuilder(); builder.loadTrustMaterial(null, new TrustStrategy() { public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { return true; } }); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), NoopHostnameVerifier.INSTANCE); HttpClientBuilder httpClientBuilder = HttpClients .custom() .setSSLSocketFactory(sslsf); if (disableRedirect) { httpClientBuilder.disableRedirectHandling(); } httpClientBuilder.setUserAgent("swagger-validator"); httpClient = httpClientBuilder.build(); } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { LOGGER.error("can't disable SSL verification", e); } return httpClient; } private String getUrlContents(String urlString) throws IOException { return getUrlContents(urlString, false, false); } private String getUrlContents(String urlString, boolean rejectLocal, boolean rejectRedirect) throws IOException { LOGGER.trace("fetching URL contents"); URL url = new URL(urlString); if(rejectLocal) { InetAddress inetAddress = InetAddress.getByName(url.getHost()); if(inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isLinkLocalAddress()) { throw new IOException("Only accepts http/https protocol"); } } final CloseableHttpClient httpClient = getCarelessHttpClient(rejectRedirect); RequestConfig.Builder requestBuilder = RequestConfig.custom(); requestBuilder = requestBuilder .setConnectTimeout(5000) .setSocketTimeout(5000); HttpGet getMethod = new HttpGet(urlString); getMethod.setConfig(requestBuilder.build()); getMethod.setHeader("Accept", "application/json, */*"); if (httpClient != null) { final CloseableHttpResponse response = httpClient.execute(getMethod); try { HttpEntity entity = response.getEntity(); StatusLine line = response.getStatusLine(); if(line.getStatusCode() > 299 || line.getStatusCode() < 200) { throw new IOException("failed to read swagger with code " + line.getStatusCode()); } return EntityUtils.toString(entity, "UTF-8"); } finally { response.close(); httpClient.close(); } } else { throw new IOException("CloseableHttpClient could not be initialized"); } } private SwaggerParseResult readOpenApi(String content) throws IllegalArgumentException { OpenAPIV3Parser parser = new OpenAPIV3Parser(); return parser.readContents(content, null, null); } private SwaggerDeserializationResult readSwagger(String content) throws IllegalArgumentException { SwaggerParser parser = new SwaggerParser(); return parser.readWithInfo(content); } private JsonNode readNode(String text) { try { if (text.trim().startsWith("{")) { return JsonMapper.readTree(text); } else { return YamlMapper.readTree(text); } } catch (IOException e) { return null; } } private String getVersion(JsonNode node) { if (node == null) { return null; } JsonNode version = node.get("openapi"); if (version != null) { return version.toString(); } version = node.get("swagger"); if (version != null) { return version.toString(); } version = node.get("swaggerVersion"); if (version != null) { return version.toString(); } LOGGER.debug("version not found!"); return null; } public String getResourceFileAsString(String fileName) { InputStream is = this.getClass().getClassLoader().getResourceAsStream(fileName); if (is != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); return reader.lines().collect(Collectors.joining(System.lineSeparator())); } return null; } }