package com.smockin.mockserver.service; import com.smockin.admin.dto.UserKeyValueDataDTO; import com.smockin.admin.persistence.entity.RestfulMock; import com.smockin.admin.service.SmockinUserService; import com.smockin.admin.service.UserKeyValueDataService; import com.smockin.mockserver.service.dto.RestfulResponseDTO; import com.smockin.utils.GeneralUtils; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import jdk.nashorn.api.scripting.ScriptObjectMirror; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URLEncodedUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import spark.Request; import javax.script.ScriptEngine; import javax.script.ScriptException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Service @Transactional public class JavaScriptResponseHandlerImpl implements JavaScriptResponseHandler { private final Logger logger = LoggerFactory.getLogger(JavaScriptResponseHandlerImpl.class); @Autowired private SmockinUserService smockinUserService; @Autowired private UserKeyValueDataService userKeyValueDataService; public RestfulResponseDTO executeUserResponse(final Request req, final RestfulMock mock) { logger.debug("executeUserResponse called"); Object engineResponse; try { engineResponse = executeJS( defaultRequestObject + populateRequestObjectWithInbound(req, mock.getPath(), mock.getCreatedBy().getCtxPath()) + populateKVPs(req, mock) + keyValuePairFindFunc + defaultResponseObject + userResponseFunctionInvoker + mock.getJavaScriptHandler().getSyntax()); } catch (ScriptException ex) { return new RestfulResponseDTO(500, "text/plain", "Looks like there is an issue with the Javascript driving this mock " + ex.getMessage()); } if (!(engineResponse instanceof ScriptObjectMirror)) { return new RestfulResponseDTO(500, "text/plain", "Looks like there is an issue with the Javascript driving this mock!"); } final ScriptObjectMirror response = (ScriptObjectMirror) engineResponse; return new RestfulResponseDTO( (int) response.get("status"), (String) response.get("contentType"), (String) response.get("body"), convertResponseHeaders(response)); } Object executeJS(final String js) throws ScriptException { if (logger.isDebugEnabled()) logger.debug(js); return buildEngine().eval(js); } String populateRequestObjectWithInbound(final Request req, final String mockPath, final String ctxPath) { final Map<String, String> reqHeaders = req.headers() .stream() .collect(Collectors.toMap(k -> k, k -> req.headers(k))); final StringBuilder reqObject = new StringBuilder(); reqObject.append("request.path=") .append("'").append(req.pathInfo()).append("'") .append("; "); if (StringUtils.isNotBlank(req.body())) { reqObject.append("request.body=") .append("'").append(req.body()).append("'") .append(";"); } final String sanitizedInboundPath = GeneralUtils.sanitizeMultiUserPath(smockinUserService.getUserMode(), req.pathInfo(), ctxPath); applyMapValuesToStringBuilder("request.pathVars", GeneralUtils.findAllPathVars(sanitizedInboundPath, mockPath), reqObject); applyMapValuesToStringBuilder("request.parameters", extractAllRequestParams(req), reqObject); applyMapValuesToStringBuilder("request.headers", reqHeaders, reqObject); return reqObject.toString(); } void applyMapValuesToStringBuilder(final String field, final Map<String, String> values, final StringBuilder reqObject) { if (values == null || values.isEmpty()) { return; } values.entrySet().forEach(e -> reqObject.append(" ") .append(field) .append("['").append(e.getKey()).append("']") .append("=") .append("'").append(e.getValue()).append("'") .append(";")); } Map<String, String> extractAllRequestParams(final Request req) { // Java Spark does not provide a convenient way of extracting form based request parameters, // so have to parse these manually. if (req.contentType() != null && (req.contentType().contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE) || req.contentType().contains(MediaType.MULTIPART_FORM_DATA_VALUE))) { return URLEncodedUtils.parse(req.body(), Charset.defaultCharset()) .stream() .collect(HashMap::new, (m, v) -> m.put(v.getName(), v.getValue()), HashMap::putAll); } return req.queryParams() .stream() .collect(Collectors.toMap(k -> k, k -> req.queryParams(k))); } Set<Map.Entry<String, String>> convertResponseHeaders(final ScriptObjectMirror response) { final Object headersJS = response.get("headers"); final Map<String, String> responseHeaders = new HashMap<>(); if (headersJS instanceof ScriptObjectMirror) { ((ScriptObjectMirror) headersJS) .entrySet() .forEach(e -> responseHeaders.put(e.getKey(), (String)e.getValue())); } return responseHeaders.entrySet(); } String populateKVPs(final Request req, final RestfulMock mock) throws ScriptException { logger.debug("populateKVPs called"); final String handleResponseFunc = GeneralUtils.removeJsComments(mock.getJavaScriptHandler().getSyntax()); final long mockOwnerUserId = mock.getCreatedBy().getId(); final int MAX_PASSES = 500; int currentPos = 0; final String keyValuePairFuncPrefix = keyValuePairFindFuncName + "("; final Map<String, String> kvps = new HashMap<>(); for (int i=0; i < MAX_PASSES; i++) { final int startPos = StringUtils.indexOf(handleResponseFunc, keyValuePairFuncPrefix, currentPos); if (startPos == -1) { break; } final int closingParenthesisPos = StringUtils.indexOf(handleResponseFunc, ")", startPos); final String sanitizedKey = findKvpKey(startPos, closingParenthesisPos, req, mock, keyValuePairFuncPrefix, handleResponseFunc); if (sanitizedKey != null) { final UserKeyValueDataDTO userKeyValueDataDTO = userKeyValueDataService.loadByKey(sanitizedKey, mockOwnerUserId); kvps.put(sanitizedKey, (userKeyValueDataDTO != null) ? userKeyValueDataDTO.getValue() : ""); } currentPos = closingParenthesisPos; } if (!kvps.isEmpty()) { return defaultKeyValuePairStoreObjectStart + GeneralUtils.serialiseJson(kvps) + ";"; } return defaultKeyValuePairStoreObject; } private String findKvpKey(final int startPos, final int closingParenthesisPos, final Request req, final RestfulMock mock, final String keyValuePairFuncPrefix, final String handleResponseFunc) throws ScriptException { logger.debug("findKvpKey called"); final String invalidMsgPrefix = "Invalid lookUpKvp(...) syntax. "; if (closingParenthesisPos == -1) { throw new ScriptException(invalidMsgPrefix + "Unable to determine closing parenthesis position"); } final String keyName = StringUtils.substring(handleResponseFunc, (startPos + keyValuePairFuncPrefix.length()), closingParenthesisPos); if (StringUtils.isBlank(keyName)) { throw new ScriptException(invalidMsgPrefix + "key within find parenthesis is undefined"); } logger.debug(String.format("keyName: %s", keyName)); final String sanitizedKey; if (keyName.startsWith("'") && keyName.endsWith("'")) { sanitizedKey = StringUtils.remove(keyName, "'"); } else if (keyName.startsWith("\"") && keyName.endsWith("\"")) { sanitizedKey = StringUtils.remove(keyName, "\""); } else if (keyName.indexOf("request.") > -1) { final String requestObjectField = StringUtils.remove(keyName, "request.").trim(); if (requestObjectField.startsWith("pathVars")) { final String pathVarsObjectField = StringUtils.remove(requestObjectField, "pathVars").trim(); final String sanitizedInboundPath = GeneralUtils.sanitizeMultiUserPath(smockinUserService.getUserMode(), req.pathInfo(), mock.getCreatedBy().getCtxPath()); sanitizedKey = GeneralUtils.findAllPathVars(sanitizedInboundPath, mock.getPath()) .get(extractObjectField(StringUtils.lowerCase(pathVarsObjectField))); } else if ("body".equals(requestObjectField)) { if (StringUtils.isBlank(req.body())) { throw new ScriptException(invalidMsgPrefix + "request.body is undefined"); } sanitizedKey = req.body(); } else if (requestObjectField.startsWith("headers")) { final String headersObjectField = StringUtils.remove(requestObjectField, "headers").trim(); sanitizedKey = req.headers() .stream() .collect(Collectors.toMap(k -> k, k -> req.headers(k))) .get(extractObjectField(headersObjectField)); } else if (requestObjectField.startsWith("parameters")) { final String parametersObjectField = StringUtils.remove(requestObjectField, "parameters").trim(); sanitizedKey = extractAllRequestParams(req).get(extractObjectField(parametersObjectField)); } else { throw new ScriptException(invalidMsgPrefix + "Unable to determine request based key look up"); } } else { throw new ScriptException(invalidMsgPrefix + "Unable to determine key lookup type"); } return (sanitizedKey != null) ? sanitizedKey.trim() : null; } private String extractObjectField(final String objectField) { if (StringUtils.startsWith(objectField, ".")) { return StringUtils.remove(objectField, "."); } else if (StringUtils.startsWith(objectField, "[")) { final String objectFieldP1 = StringUtils.remove(objectField, "['"); return StringUtils.remove(objectFieldP1, "']"); } return null; } private ScriptEngine buildEngine() { return new NashornScriptEngineFactory() .getScriptEngine(engineSecurityArgs); } }