package io.swagger.validator.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.parser.SwaggerParser;
import io.swagger.parser.util.SwaggerDeserializationResult;
import io.swagger.util.Json;
import io.swagger.util.Yaml;
import io.swagger.validator.models.SchemaValidationError;
import io.swagger.validator.models.ValidationResponse;
import org.apache.commons.io.IOUtils;
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.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.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class ValidatorService {
static final String INVALID_VERSION = "Deprecated Swagger version. Please visit http://swagger.io for information on upgrading to Swagger 2.0\"";
static final String SCHEMA_FILE = "schema.json";
static final String SCHEMA_URL = "http://swagger.io/v2/schema.json";
static Logger LOGGER = LoggerFactory.getLogger(ValidatorService.class);
static long LAST_FETCH = 0;
static String CACHED_SCHEMA = null;
static ObjectMapper JsonMapper = Json.mapper();
static ObjectMapper YamlMapper = Yaml.mapper();
private JsonSchema schema;
public void validateByUrl(HttpServletRequest request, HttpServletResponse response, String url) {
LOGGER.info("validationUrl: " + url + ", forClient: " + getRemoteAddr(request));
ValidationResponse payload = null;
try {
payload = debugByUrl(request, response, url);
}
catch (Exception e) {
error(response);
}
if(payload == null) {
fail(response);
return;
}
if(payload.getMessages() == null && payload.getSchemaValidationMessages() == null) {
success(response);
}
// some values may be unsupported, and that shouldn't invalidate the spec
if(payload.getMessages() != null && payload.getMessages().size() > 0) {
boolean valid = true;
for(String message : payload.getMessages()) {
if(!message.endsWith("is unsupported")) {
valid = false;
}
}
if(valid) {
success(response);
}
}
if(payload.getSchemaValidationMessages() != null) {
for(SchemaValidationError message : payload.getSchemaValidationMessages()) {
if(INVALID_VERSION.equals(message)) {
upgrade(response);
}
}
}
error(response);
}
private String getVersion(JsonNode node) {
if (node == null) {
return null;
}
JsonNode 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 ValidationResponse debugByUrl(HttpServletRequest request, HttpServletResponse response, 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);
} 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;
}
// convert to a JsonNode
JsonNode schemaObject = JsonMapper.readTree(getSchema());
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;
}
// get the version, return deprecated if version 1.x
String version = getVersion(spec);
if (version != null && version.startsWith("\"1.")) {
ProcessingMessage pm = new ProcessingMessage();
pm.setLogLevel(LogLevel.ERROR);
pm.setMessage(INVALID_VERSION);
output.addValidationMessage(new SchemaValidationError(pm.asJson()));
return output;
}
// use the swagger deserializer to get human-friendly messages
SwaggerDeserializationResult result = readSwagger(content);
if(result != null) {
for(String message : result.getMessages()) {
output.addMessage(message);
}
}
// do actual JSON schema validation
JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
JsonSchema schema = factory.getJsonSchema(schemaObject);
ProcessingReport report = schema.validate(spec);
ListProcessingReport lp = new ListProcessingReport();
lp.mergeWith(report);
if (report.isSuccess()) {
try {
readSwagger(content);
} catch (IllegalArgumentException e) {
LOGGER.debug("can't read swagger contents", e);
ProcessingMessage pm = new ProcessingMessage();
pm.setLogLevel(LogLevel.ERROR);
pm.setMessage("unable to parse swagger from " + url);
output.addValidationMessage(new SchemaValidationError(pm.asJson()));
return output;
}
}
java.util.Iterator<ProcessingMessage> it = lp.iterator();
while (it.hasNext()) {
ProcessingMessage pm = it.next();
output.addValidationMessage(new SchemaValidationError(pm.asJson()));
}
return output;
}
public ValidationResponse debugByContent(HttpServletRequest request, HttpServletResponse response, String content) throws Exception {
JsonNode schemaObject = JsonMapper.readTree(getSchema());
JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
JsonSchema schema = factory.getJsonSchema(schemaObject);
ValidationResponse output = new ValidationResponse();
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;
}
// use the swagger deserializer to get human-friendly messages
SwaggerDeserializationResult result = readSwagger(content);
if(result != null) {
for(String message : result.getMessages()) {
output.addMessage(message);
}
}
// do actual JSON schema validation
ProcessingReport report = schema.validate(spec);
ListProcessingReport lp = new ListProcessingReport();
lp.mergeWith(report);
if (report.isSuccess()) {
try {
readSwagger(content);
} catch (IllegalArgumentException e) {
LOGGER.debug("can't read swagger contents", e);
ProcessingMessage pm = new ProcessingMessage();
pm.setLogLevel(LogLevel.ERROR);
pm.setMessage("unable to parse swagger from contents");
output.addValidationMessage(new SchemaValidationError(pm.asJson()));
return output;
}
}
java.util.Iterator<ProcessingMessage> it = lp.iterator();
while (it.hasNext()) {
ProcessingMessage pm = it.next();
output.addValidationMessage(new SchemaValidationError(pm.asJson()));
}
return output;
}
private void success(HttpServletResponse response) {
writeToResponse(response, "valid.png");
}
private void error(HttpServletResponse response) {
writeToResponse(response, "error.png");
}
private void fail(HttpServletResponse response) {
writeToResponse(response, "invalid.png");
}
private void upgrade(HttpServletResponse response) {
writeToResponse(response, "upgrade.png");
}
private void writeToResponse(HttpServletResponse response, String name) {
try {
InputStream is = this.getClass().getClassLoader().getResourceAsStream(name);
if (is != null) {
IOUtils.copy(is, response.getOutputStream());
}
} catch (IOException e) {
LOGGER.error("can't send response image", e);
}
}
private String getSchema() throws Exception {
if (CACHED_SCHEMA != null && (System.currentTimeMillis() - LAST_FETCH) < 600000) {
return CACHED_SCHEMA;
}
try {
LOGGER.debug("returning cached schema");
LAST_FETCH = System.currentTimeMillis();
CACHED_SCHEMA = getUrlContents(SCHEMA_URL);
return CACHED_SCHEMA;
} catch (Exception e) {
LOGGER.warn("fetching schema from GitHub");
InputStream is = this.getClass().getClassLoader().getResourceAsStream(SCHEMA_FILE);
BufferedReader in = new BufferedReader(
new InputStreamReader(is));
String inputLine;
StringBuilder contents = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
contents.append(inputLine);
}
in.close();
LAST_FETCH = System.currentTimeMillis();
CACHED_SCHEMA = contents.toString();
return CACHED_SCHEMA;
}
}
private CloseableHttpClient getCarelessHttpClient() {
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);
httpClient = HttpClients
.custom()
.setSSLSocketFactory(sslsf)
.build();
} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
LOGGER.error("can't disable SSL verification", e);
}
return httpClient;
}
private String getUrlContents(String urlString) throws IOException {
LOGGER.trace("fetching URL contents");
final CloseableHttpClient httpClient = getCarelessHttpClient();
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");
}
}
protected String getRemoteAddr(HttpServletRequest request) {
String ipAddress = request.getHeader("X-FORWARDED-FOR");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
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;
}
}
}