From 0b7826c0f926a30991c54c9238854fbfc1c136fd Mon Sep 17 00:00:00 2001 From: WangXu10 Date: Tue, 27 Aug 2024 15:25:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=B0=83=E8=AF=95=E5=AF=BC=E5=85=A5curl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/i18n/api_en_US.properties | 6 +- .../main/resources/i18n/api_zh_CN.properties | 6 +- .../main/resources/i18n/api_zh_TW.properties | 6 +- .../controller/debug/ApiDebugController.java | 13 ++ .../curl/constants/CurlPatternConstants.java | 73 +++++++ .../api/curl/domain/CurlEntity.java | 50 +++++ .../api/curl/handler/CurlHandlerChain.java | 76 +++++++ .../api/curl/handler/HeaderHandler.java | 61 ++++++ .../api/curl/handler/HttpBodyHandler.java | 186 ++++++++++++++++++ .../api/curl/handler/HttpMethodHandler.java | 41 ++++ .../api/curl/handler/ICurlHandler.java | 13 ++ .../api/curl/handler/QueryParamsHandler.java | 63 ++++++ .../api/curl/handler/UrlPathHandler.java | 34 ++++ .../api/curl/util/CurlParserUtil.java | 30 +++ .../api/dto/request/ApiImportCurlRequest.java | 17 ++ .../controller/ApiDebugControllerTests.java | 163 +++++++++++++++ 16 files changed, 835 insertions(+), 3 deletions(-) create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/constants/CurlPatternConstants.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/domain/CurlEntity.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/CurlHandlerChain.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HeaderHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpBodyHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpMethodHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/ICurlHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/QueryParamsHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/UrlPathHandler.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/curl/util/CurlParserUtil.java create mode 100644 backend/services/api-test/src/main/java/io/metersphere/api/dto/request/ApiImportCurlRequest.java diff --git a/backend/framework/sdk/src/main/resources/i18n/api_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/api_en_US.properties index 54954161ec..f109b923ac 100644 --- a/backend/framework/sdk/src/main/resources/i18n/api_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/api_en_US.properties @@ -452,4 +452,8 @@ api_definition.status.abandoned=Abandoned api_definition.status.continuous=Continuous api_test_case.clear.api_change=Ignore the differences in this change -api_test_case.ignore.api_change=Ignore all change differences \ No newline at end of file +api_test_case.ignore.api_change=Ignore all change differences + +curl_script_is_empty=Curl script cannot be empty +curl_script_is_invalid=Curl script is invalid +curl_raw_content_is_invalid=Raw content is invalid \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/api_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/api_zh_CN.properties index ee13a0110a..4fdfcc4704 100644 --- a/backend/framework/sdk/src/main/resources/i18n/api_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/api_zh_CN.properties @@ -420,4 +420,8 @@ api_definition.status.abandoned=已废弃 api_definition.status.continuous=连调中 api_test_case.clear.api_change=忽略本次变更差异 -api_test_case.ignore.api_change=忽略全部变更差异 \ No newline at end of file +api_test_case.ignore.api_change=忽略全部变更差异 + +curl_script_is_empty=cURL脚本不能为空 +curl_script_is_invalid=cURL脚本格式不正确 +curl_raw_content_is_invalid=raw内容格式不正确 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/api_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/api_zh_TW.properties index d214cbb8bb..4d1592e03d 100644 --- a/backend/framework/sdk/src/main/resources/i18n/api_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/api_zh_TW.properties @@ -420,4 +420,8 @@ api_definition.status.abandoned=已作廢 api_definition.status.continuous=持續中 api_test_case.clear.api_change=忽略本次變更差異 -api_test_case.ignore.api_change=忽略全部變更差異 \ No newline at end of file +api_test_case.ignore.api_change=忽略全部變更差異 + +curl_script_is_empty=curl脚本不能爲空 +curl_script_is_invalid=curl脚本格式不正確 +curl_raw_content_is_invalid=raw内容格式不正確 \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/controller/debug/ApiDebugController.java b/backend/services/api-test/src/main/java/io/metersphere/api/controller/debug/ApiDebugController.java index 3ec34c4416..ba51e05c97 100644 --- a/backend/services/api-test/src/main/java/io/metersphere/api/controller/debug/ApiDebugController.java +++ b/backend/services/api-test/src/main/java/io/metersphere/api/controller/debug/ApiDebugController.java @@ -1,8 +1,11 @@ package io.metersphere.api.controller.debug; +import io.metersphere.api.curl.domain.CurlEntity; +import io.metersphere.api.curl.util.CurlParserUtil; import io.metersphere.api.domain.ApiDebug; import io.metersphere.api.dto.debug.*; import io.metersphere.api.dto.request.ApiEditPosRequest; +import io.metersphere.api.dto.request.ApiImportCurlRequest; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.service.ApiFileResourceService; import io.metersphere.api.service.debug.ApiDebugLogService; @@ -120,4 +123,14 @@ public class ApiDebugController { public List options(@PathVariable String projectId) { return fileModuleService.getTree(projectId); } + + + @PostMapping("/import-curl") + @Operation(summary = "接口测试-接口调试-导入curl") + @RequiresPermissions(PermissionConstants.PROJECT_API_DEBUG_IMPORT) + public CurlEntity importCurl(@RequestBody ApiImportCurlRequest request) { + CurlEntity parse = CurlParserUtil.parse(request.getCurl()); + return parse; + } + } \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/constants/CurlPatternConstants.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/constants/CurlPatternConstants.java new file mode 100644 index 0000000000..c0789b3946 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/constants/CurlPatternConstants.java @@ -0,0 +1,73 @@ +package io.metersphere.api.curl.constants; + +import java.util.regex.Pattern; + +/** + * @author wx + */ +public interface CurlPatternConstants { + + /** + * CURL结构校验 + */ + Pattern CURL_STRUCTURE_PATTERN = Pattern.compile("^curl"); + + /** + * URL路径 + */ + Pattern URL_PATH_PATTERN = Pattern.compile("(?:\\s|^)(?:'|\")?(https?://[^\\s'\"]*(?:\\?[^\\s'\"]*)?)(?:'|\")?(?:\\s|$)"); + + /** + * URL_PARAMS请求参数 + */ + Pattern URL_PARAMS_PATTERN = Pattern.compile("(?:\\s|^)(?:'|\")?(https?://[^\\s'\"]+)(?:'|\")?(?:\\s|$)"); + + /** + * HTTP请求方法 + */ + Pattern HTTP_METHOD_PATTERN = Pattern.compile("curl\\s+[^\\s]*\\s+(?:-X|--request)\\s+'?(GET|POST)'?"); + + /** + * 默认HTTP请求方法 + */ + Pattern DEFAULT_HTTP_METHOD_PATTERN = Pattern.compile(".*\\s(-d|--data|--data-binary)\\s.*"); + + /** + * 请求头 + */ + Pattern CURL_HEADERS_PATTERN = Pattern.compile("(?:-H|--header)\\s+(?:\"([^\"]*)\"|'([^']*)')"); + + /** + * -u/--user 请求头 + */ + Pattern CURL_USER_HEAD_PATTERN = Pattern.compile("-(u|user)\\s+(\\S+:\\S+)"); + + /** + * -d/--data 请求体 + */ + Pattern DEFAULT_HTTP_BODY_PATTERN = Pattern.compile("(?:--data|-d)\\s+(?:'([^']*)'|\"([^\"]*)\"|(\\S+))", Pattern.DOTALL); + Pattern DEFAULT_HTTP_BODY_PATTERN_KV = Pattern.compile("^([^=&]+=[^=&]+)(?:&[^=&]+=[^=&]+)*$", Pattern.DOTALL); + + /** + * --data-raw 请求体 + */ + Pattern HTTP_ROW_BODY_PATTERN = Pattern.compile("--data-raw '(.+?)'(?s)", Pattern.DOTALL); + + /** + * --form 请求体 + */ + Pattern HTTP_FROM_BODY_PATTERN = Pattern.compile("--form\\s+'(.*?)'|-F\\s+'(.*?)'"); + + + /** + * --data-urlencode 请求体 + */ + Pattern HTTP_URLENCODE_BODY_PATTERN = Pattern.compile("--data-urlencode\\s+'(.*?)'"); + + + /** + * -x/--proxy 代理配置 + */ + Pattern PROXY_PATTERN = Pattern.compile("(-x|--proxy)\\s+[\\S]+"); + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/domain/CurlEntity.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/domain/CurlEntity.java new file mode 100644 index 0000000000..ae7a8447e5 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/domain/CurlEntity.java @@ -0,0 +1,50 @@ +package io.metersphere.api.curl.domain; + +import lombok.Builder; +import lombok.Data; +import org.json.JSONObject; + +import java.util.Map; + +/** + * @author wx + */ +@Data +@Builder +public class CurlEntity { + /** + * URL路径 + */ + private String url; + + /** + * 请求方法类型 + */ + private Method method; + + /** + * URL参数 + */ + private Map queryParams; + + /** + * header参数 + */ + private Map headers; + + /** + * 请求体 + */ + private JSONObject body; + + public enum Method { + GET, + POST, + PUT, + DELETE, + PATCH, + OPTIONS, + HEAD, + CONNECT + } +} diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/CurlHandlerChain.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/CurlHandlerChain.java new file mode 100644 index 0000000000..7cd710ea48 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/CurlHandlerChain.java @@ -0,0 +1,76 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; +import org.apache.commons.lang3.StringUtils; + +import java.util.regex.Matcher; + +/** + * @author wx + */ +public abstract class CurlHandlerChain implements ICurlHandler { + + ICurlHandler next; + + @Override + public ICurlHandler next(ICurlHandler handler) { + this.next = handler; + return this.next; + } + + @Override + public abstract void handle(CurlEntity entity, String curl); + + + protected void nextHandle(CurlEntity curlEntity, String curl) { + if (next != null) { + next.handle(curlEntity, curl); + } + } + + protected void validate(String curl) { + if (StringUtils.isBlank(curl)) { + throw new MSException(Translator.get("curl_script_is_empty")); + } + + Matcher matcher = CurlPatternConstants.CURL_STRUCTURE_PATTERN.matcher(curl); + if (!matcher.find()) { + throw new MSException(Translator.get("curl_script_is_invalid")); + } + } + + public static CurlHandlerChain init() { + return new CurlHandlerChain() { + @Override + public void handle(CurlEntity entity, String curl) { + this.validate(curl); + + // 替换掉可能存在的转译(字符串中的空白字符,包括空格、换行符和制表符...) + curl = curl.replace("\\", "") + .replace("\n", "") + .replace("\t", ""); + + Matcher matcher = CurlPatternConstants.PROXY_PATTERN.matcher(curl); + if (matcher.find()) { + curl = matcher.replaceAll("").trim(); + } + + int compressedIndex = curl.indexOf("--compressed"); + if (compressedIndex != -1) { + String beforeCompressed = curl.substring(4, compressedIndex); + String afterCompressed = curl.substring(compressedIndex + "--compressed".length()); + curl = "curl" + afterCompressed + beforeCompressed; + } + + + if (next != null) { + next.handle(entity, curl); + } + } + }; + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HeaderHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HeaderHandler.java new file mode 100644 index 0000000000..b2772f3371 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HeaderHandler.java @@ -0,0 +1,61 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; +import org.apache.commons.lang3.StringUtils; + +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; + +/** + * @author wx + */ +public class HeaderHandler extends CurlHandlerChain { + + @Override + public void handle(CurlEntity entity, String curl) { + Map headers = parseHeaders(curl); + entity.setHeaders(headers); + super.nextHandle(entity, curl); + } + + /** + * header解析 + * + * @param curl + * @return + */ + private Map parseHeaders(String curl) { + if (StringUtils.isBlank(curl)) { + return Collections.emptyMap(); + } + + Matcher matcher = CurlPatternConstants.CURL_HEADERS_PATTERN.matcher(curl); + Map headers = new HashMap<>(); + while (matcher.find()) { + String header = ""; + if (matcher.group(1) != null) { + header = matcher.group(1); + } else { + header = matcher.group(2); + } + String[] headerKeyValue = header.split(":", 2); + if (headerKeyValue.length == 2) { + // 去除键和值的首尾空白字符 + headers.put(headerKeyValue[0].trim(), headerKeyValue[1].trim()); + } + } + + Matcher userMatcher = CurlPatternConstants.CURL_USER_HEAD_PATTERN.matcher(curl); + if (userMatcher.find()) { + String user = userMatcher.group(2); + headers.put("Authorization", "Basic " + Base64.getEncoder().encodeToString(user.getBytes())); + } + + return headers; + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpBodyHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpBodyHandler.java new file mode 100644 index 0000000000..4ab8492693 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpBodyHandler.java @@ -0,0 +1,186 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; +import io.metersphere.api.utils.JSONUtil; +import io.metersphere.sdk.exception.MSException; +import io.metersphere.sdk.util.Translator; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.XML; +import org.xml.sax.InputSource; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; + + +/** + * @author wx + */ +public class HttpBodyHandler extends CurlHandlerChain { + @Override + public void handle(CurlEntity entity, String curl) { + JSONObject body = parseBody(curl); + entity.setBody(body); + super.nextHandle(entity, curl); + } + + /** + * 请求体解析 + * + * @param curl + * @return + */ + private JSONObject parseBody(String curl) { + Matcher formMatcher = CurlPatternConstants.HTTP_FROM_BODY_PATTERN.matcher(curl); + if (formMatcher.find()) { + return parseFormBody(formMatcher); + } + + Matcher urlencodeMatcher = CurlPatternConstants.HTTP_URLENCODE_BODY_PATTERN.matcher(curl); + if (urlencodeMatcher.find()) { + return parseUrlEncodeBody(urlencodeMatcher); + } + + Matcher rawMatcher = CurlPatternConstants.HTTP_ROW_BODY_PATTERN.matcher(curl); + if (rawMatcher.find()) { + return parseRowBody(rawMatcher); + } + + Matcher defaultMatcher = CurlPatternConstants.DEFAULT_HTTP_BODY_PATTERN.matcher(curl); + if (defaultMatcher.find()) { + return parseDefaultBody(defaultMatcher); + } + + return new JSONObject(); + } + + private JSONObject parseDefaultBody(Matcher defaultMatcher) { + String bodyStr = ""; + if (defaultMatcher.group(1) != null) { + //单引号数据 + bodyStr = defaultMatcher.group(1); + } else if (defaultMatcher.group(2) != null) { + //双引号数据 + bodyStr = defaultMatcher.group(2); + } else { + //无引号数据 + bodyStr = defaultMatcher.group(3); + } + + if (isJSON(bodyStr)) { + return JSONUtil.parseObject(bodyStr); + } + + //其他格式 a=b&c=d + Matcher kvMatcher = CurlPatternConstants.DEFAULT_HTTP_BODY_PATTERN_KV.matcher(bodyStr); + return kvMatcher.matches() ? parseKVBody(bodyStr) : new JSONObject(); + } + + private JSONObject parseKVBody(String kvBodyStr) { + JSONObject json = new JSONObject(); + String[] pairs = kvBodyStr.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8); + String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8); + json.put(key, value); + } + return json; + } + + private JSONObject parseFormBody(Matcher formMatcher) { + JSONObject formData = new JSONObject(); + + formMatcher.reset(); + while (formMatcher.find()) { + //提取表单 + String formItem = formMatcher.group(1) != null ? formMatcher.group(1) : formMatcher.group(2); + String[] keyValue = formItem.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + + //文件属性 + if (value.startsWith("@")) { + //获取文件名 + formData.put(key, value.substring(1)); + } else { + formData.put(key, value); + } + } + } + + return formData; + } + + private JSONObject parseUrlEncodeBody(Matcher urlencodeMatcher) { + JSONObject urlEncodeData = new JSONObject(); + urlencodeMatcher.reset(); + while (urlencodeMatcher.find()) { + String keyValueEncoded = urlencodeMatcher.group(1); + String[] keyValue = keyValueEncoded.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8); + urlEncodeData.put(key, decodedValue); + } + } + return urlEncodeData; + } + + private JSONObject parseRowBody(Matcher rowMatcher) { + String rawData = rowMatcher.group(1); + + if (isXML(rawData)) { + return xml2json(rawData); + } + + try { + return JSONUtil.parseObject(rawData); + } catch (Exception e) { + throw new MSException(Translator.get("curl_raw_content_is_invalid"), e); + } + } + + private boolean isJSON(String jsonStr) { + try { + JSONUtil.parseObject(jsonStr); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean isXML(String xmlStr) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("disallow-doctype-decl", false); + factory.setFeature("external-general-entities", false); + factory.setFeature("external-parameter-entities", false); + + DocumentBuilder builder = factory.newDocumentBuilder(); + InputSource is = new InputSource(new StringReader(xmlStr)); + builder.parse(is); + return true; + } catch (Exception e) { + return false; + } + } + + private JSONObject xml2json(String xmlStr) { + try { + JSONObject orgJsonObj = XML.toJSONObject(xmlStr); + String jsonString = orgJsonObj.toString(); + return JSONUtil.parseObject(jsonString); + } catch (JSONException e) { + throw new MSException(Translator.get("curl_raw_content_is_invalid"), e); + } + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpMethodHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpMethodHandler.java new file mode 100644 index 0000000000..9e3c49b88a --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/HttpMethodHandler.java @@ -0,0 +1,41 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; + +import java.util.regex.Matcher; + +/** + * @author wx + */ +public class HttpMethodHandler extends CurlHandlerChain { + + @Override + public void handle(CurlEntity entity, String curl) { + CurlEntity.Method method = parseMethod(curl); + entity.setMethod(method); + super.nextHandle(entity, curl); + } + + /** + * 请求方法解析 + * + * @param curl + * @return + */ + private CurlEntity.Method parseMethod(String curl) { + Matcher matcher = CurlPatternConstants.HTTP_METHOD_PATTERN.matcher(curl); + Matcher defaultMatcher = CurlPatternConstants.DEFAULT_HTTP_METHOD_PATTERN.matcher(curl); + if (matcher.find()) { + String method = matcher.group(1); + return CurlEntity.Method.valueOf(method.toUpperCase()); + } else if (defaultMatcher.find()) { + //如果命令中包含 -d 或 --data,没有明确请求方法,默认为 POST + return CurlEntity.Method.POST; + } else { + //没有明确指定请求方法,默认为 GET + return CurlEntity.Method.GET; + } + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/ICurlHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/ICurlHandler.java new file mode 100644 index 0000000000..d0d0269daa --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/ICurlHandler.java @@ -0,0 +1,13 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.domain.CurlEntity; + +/** + * @author wx + */ +public interface ICurlHandler { + + ICurlHandler next(ICurlHandler handler); + + void handle(CurlEntity entity, String curl); +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/QueryParamsHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/QueryParamsHandler.java new file mode 100644 index 0000000000..912e10aaa5 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/QueryParamsHandler.java @@ -0,0 +1,63 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; + +/** + * @author wx + */ +public class QueryParamsHandler extends CurlHandlerChain { + + @Override + public void handle(CurlEntity entity, String curl) { + String url = extractUrl(curl); + Map queryParams = parseQueryParams(url); + entity.setQueryParams(queryParams); + super.nextHandle(entity, curl); + } + + private String extractUrl(String curl) { + Matcher matcher = CurlPatternConstants.URL_PARAMS_PATTERN.matcher(curl); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + /** + * query参数解析 + * + * @param url + * @return + */ + private Map parseQueryParams(String url) { + if (StringUtils.isBlank(url)) { + return Collections.emptyMap(); + } + + Map queryParams = new HashMap<>(); + String[] urlParts = url.split("\\?"); + if (urlParts.length > 1) { + String query = urlParts[1]; + String[] pairs = query.split("&"); + for (String pair : pairs) { + int idx = pair.indexOf("="); + if (idx != -1 && idx < pair.length() - 1) { + String key = pair.substring(0, idx); + String value = pair.substring(idx + 1); + queryParams.put(key, value); + } else { + queryParams.put(pair, null); + } + } + } + return queryParams; + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/UrlPathHandler.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/UrlPathHandler.java new file mode 100644 index 0000000000..4fe4a193cd --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/handler/UrlPathHandler.java @@ -0,0 +1,34 @@ +package io.metersphere.api.curl.handler; + +import io.metersphere.api.curl.constants.CurlPatternConstants; +import io.metersphere.api.curl.domain.CurlEntity; + +import java.util.regex.Matcher; + +/** + * @author wx + */ +public class UrlPathHandler extends CurlHandlerChain { + + @Override + public void handle(CurlEntity entity, String curl) { + String url = parseUrlPath(curl); + entity.setUrl(url); + super.nextHandle(entity, curl); + } + + /** + * url路径解析 + * + * @param curl + * @return + */ + private String parseUrlPath(String curl) { + Matcher matcher = CurlPatternConstants.URL_PATH_PATTERN.matcher(curl); + if (matcher.find()) { + return matcher.group(1) != null ? matcher.group(1) : matcher.group(3); + } + return null; + } + +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/curl/util/CurlParserUtil.java b/backend/services/api-test/src/main/java/io/metersphere/api/curl/util/CurlParserUtil.java new file mode 100644 index 0000000000..5167026ebf --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/curl/util/CurlParserUtil.java @@ -0,0 +1,30 @@ +package io.metersphere.api.curl.util; + +import io.metersphere.api.curl.domain.CurlEntity; +import io.metersphere.api.curl.handler.*; + +/** + * @author wx + */ +public class CurlParserUtil { + + + /** + * 解析crul 工具类 + * @param curl + * @return + */ + public static CurlEntity parse(String curl) { + CurlEntity entity = CurlEntity.builder().build(); + ICurlHandler handlerChain = CurlHandlerChain.init(); + + handlerChain.next(new UrlPathHandler()) + .next(new QueryParamsHandler()) + .next(new HttpMethodHandler()) + .next(new HeaderHandler()) + .next(new HttpBodyHandler()); + + handlerChain.handle(entity, curl); + return entity; + } +} \ No newline at end of file diff --git a/backend/services/api-test/src/main/java/io/metersphere/api/dto/request/ApiImportCurlRequest.java b/backend/services/api-test/src/main/java/io/metersphere/api/dto/request/ApiImportCurlRequest.java new file mode 100644 index 0000000000..33bc589b54 --- /dev/null +++ b/backend/services/api-test/src/main/java/io/metersphere/api/dto/request/ApiImportCurlRequest.java @@ -0,0 +1,17 @@ +package io.metersphere.api.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author wx + */ +@Data +public class ApiImportCurlRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "curl字符串") + private String curl; +} diff --git a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDebugControllerTests.java b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDebugControllerTests.java index 057ee698c2..5d5533845a 100644 --- a/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDebugControllerTests.java +++ b/backend/services/api-test/src/test/java/io/metersphere/api/controller/ApiDebugControllerTests.java @@ -12,6 +12,7 @@ import io.metersphere.api.dto.debug.*; import io.metersphere.api.dto.definition.ResponseBinaryBody; import io.metersphere.api.dto.definition.ResponseBody; import io.metersphere.api.dto.request.ApiEditPosRequest; +import io.metersphere.api.dto.request.ApiImportCurlRequest; import io.metersphere.api.dto.request.ApiTransferRequest; import io.metersphere.api.dto.request.MsCommonElement; import io.metersphere.api.dto.request.http.MsHTTPElement; @@ -94,6 +95,8 @@ public class ApiDebugControllerTests extends BaseTest { public static final String TRANSFER_OPTION = "transfer/options"; public static final String TRANSFER = "transfer"; + public static final String IMPORT_CURL = "import-curl"; + @Resource private ApiDebugMapper apiDebugMapper; @Resource @@ -782,4 +785,164 @@ public class ApiDebugControllerTests extends BaseTest { requestGetPermissionTest(PermissionConstants.PROJECT_API_DEBUG_DELETE, DEFAULT_DELETE, addApiDebug.getId()); } + + @Test + @Order(18) + public void testImportCurl() throws Exception { + ApiImportCurlRequest request = new ApiImportCurlRequest(); + //浏览器 curl 测试 + String curl = "curl 'https://127.0.0.1:8081/api/definition/page' \\\n" + + " -H 'Accept: application/json, text/plain, */*' \\\n" + + " -H 'Accept-Language: zh-CN' \\\n" + + " -H 'CSRF-TOKEN: 1234454351313131431' \\\n" + + " -H 'Connection: keep-alive' \\\n" + + " -H 'Content-Type: application/json;charset=UTF-8' \\\n" + + " -H 'ORGANIZATION: 100001' \\\n" + + " -H 'Origin: http://127.0.0.1:8081' \\\n" + + " -H 'PROJECT: 100001100001' \\\n" + + " -H 'Referer: http://127.0.0.1:8081/' \\\n" + + " -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' \\\n" + + " -H 'X-AUTH-TOKEN: 45fdgsrgdsg-2baf-40bc-98ba-5dsg15s1fg' \\\n" + + " --data-raw '{\"current\":1,\"pageSize\":10,\"sort\":{},\"keyword\":\"\",\"combine\":{},\"searchMode\":\"AND\",\"projectId\":\"100001100001\",\"moduleIds\":[],\"protocols\":[\"HTTP\"],\"filter\":{\"status\":[],\"method\":[],\"priority\":[]},\"excludeIds\":[\"123456783242123\",\"\",\"\"]}' \\\n" + + " --insecure"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl 'http://127.0.0.1:8081/project/get-member/option/100001100001?_t=1724293604633' \\\n" + + " -H 'Accept: application/json, text/plain, */*' \\\n" + + " -H 'Accept-Language: zh-CN' \\\n" + + " -H 'CSRF-TOKEN: Q+DnK2GzMwG07cIVmaaeqSHZFeOk6RnorsyXL9eSCASECASDFJzHzwj60q9uW43o/yESDFSCESASDSFASDH3xXTiCXRxPXT6spuFIHjmYQ+AYbw=' \\\n" + + " -H 'ORGANIZATION: 100001' \\\n" + + " -H 'PROJECT: 1202136548611' \\\n" + + " -H 'Proxy-Connection: keep-alive' \\\n" + + " -H 'Referer: http://127.0.0.1:8081/' \\\n" + + " -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' \\\n" + + " -H 'X-AUTH-TOKEN: 85de962d-2baf-40bc-98ba-9af2e6564d0b' \\\n" + + " --insecure"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //抓包格式测试 + //Charles工具 curl 测试 + curl = "curl \n" + + "-H 'Host: 127.0.0.1:8081' \n" + + "-H 'Accept: application/json, text/plain, */*' \n" + + "-H 'CSRF-TOKEN: Q+DnK2GzMwG07cIVmaaeqSHZFeOk6RnorsyXL9eD9VxP3FEJzHzwj60q9uW43o/y0Exoa6kQub0sN0H3xXTiCXRxPXT6spuFIHjmYQ+AYbw=' \n" + + "-H 'X-AUTH-TOKEN: 512dsfsfds255d-2baf-40bc-98ba-5dsg15s1fg' \n" + + "-H 'PROJECT: 124548721548' \n" + + "-H 'Accept-Language: zh-CN' \n" + + "-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' \n" + + "-H 'ORGANIZATION: 100001' -H 'Referer: http://127.0.0.1:8081/' \n" + + "--compressed 'http://127.0.0.1:8081/project/get/100001100001?_t=1724294013069'"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //Fiddler工具 curl 测试 + curl = "curl -X POST 'http://example.com/api' -H 'Accept: application/json' -H 'User-Agent: Fiddler' -H 'Authorization: Bearer token_here'"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //GET 请求 测试 + curl = "curl -X GET https://example.com"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //POST 请求带数据 测试 + curl = "curl -X POST -d 'key1=value1&key2=value2' https://example.com/post"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //json数据 测试 + curl = "curl -X POST -H 'Content-Type: application/json' -d '{\"key\":\"value\"}' https://example.com/post"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //文件 测试 + curl = "curl -F 'file=@path/to/file' https://example.com/upload"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //自定义头部和认证 测试 + curl = "curl -H 'Authorization: Bearer token' -H 'Accept: application/json' -u username:password https://example.com"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + //组合 测试 + curl = "curl -X POST \\\n" + + "-u username:password \\\n" + + "-H 'Content-Type: multipart/form-data' \\\n" + + "-H 'Custom-Header: Value' \\\n" + + "-F 'file=@/path/to/file' \\\n" + + "-F 'param1=value1' \\\n" + + "https://example.com/upload"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl -X GET \\\n" + + "-H 'Authorization: Bearer YOUR_TOKEN' \\\n" + + "-L \\\n" + + "-v \\\n" + + "https://example.com/resource"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl -X GET \\\n" + + "-H 'Accept: application/json' \\\n" + + "-x http://proxyserver:port \\\n" + + "-i \\\n" + + "https://example.com/api?param1=value1¶m2=value2"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl -X POST \\\n" + + "-H 'Content-Type: application/json' \\\n" + + "-d '{\"key1\": \"value1\", \"key2\": \"value2\"}' \\\n" + + "--max-time 30 \\\n" + + "--retry 3 \\\n" + + "https://example.com/api"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl 'https://www.tapd.cn/2335412151/prong/iterations/card_view/123465456789431534?q=fsadfasjhkahkrhfdsasccasfsdf' \\\n" + + " -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \\\n" + + " -H 'Accept-Language: zh-CN,zh;q=0.9' \\\n" + + " -H 'Cache-Control: max-age=0' \\\n" + + " -H 'Connection: keep-alive' \\\n" + + " -H 'Cookie: iter_card_status=; 66565258464512_55234234_iterations_card_view_close_status=0; 232564132154_55023423_/prong/iterations/index_remember_view=134247554678224278546; iter_card_status=; 412045464_5123433_iterations_card_view_close_status=0; left_iteration_list_token=20225424528746eda6e21g1dfgd51867891ef7cbb3a9; tui_filter_fields=%5B%13d1gd51r1gf23d1r%2C%22owner%22%2C%22dg13dr51ation_id2C%dsf22priority%22%5D; 112315sc39_5501533_/prong/tasks/index_remember_view=115501223315036973; 5dfsse933_11324fsd3001000025_story_create_template=1155041233424001000009; tapdsession=17174887199dc61sdfscaeerg229dcd5beb5c16d6062b32d6cesc68a61f51fb; __root_domain_v=.tapd.cn; _qddaz=QD.147917cscse0554; t_u=7ab057cd1f0c6casedfcasfr61d09eb29f94c12a82c73007d3e505f68411bdfgdrg156f1b984a7b98566b7bdsafs2937ccb1974809f3fsef3f828%7C1; iteration_view_type_cookie=card_view; fsefdcdcbug_create_template=1155049f12055242000010; new_worktable=search_filter; dsc-token=V0FahgEQeO8hNyzI; 5532133_11550434242340025_story_create_template=115504234234000009; iteration_card_tab_33242490=list; iteration_card_current_iteration_334235590=--; cloud_current_workspaceId=53429234; iteration_card_tab_324234dd=list; _t_uid=19732439; _t_crop=6049432436; tapd_div=101_2885; locale=zh_CN; iteration_card_current_iteration_5234234=1155042344512542342863' \\\n" + + " -H 'Referer: https://www.tapd.cn/5324120234165/bugtrace/bugs/view?bug_id=1445248132543744315' \\\n" + + " -H 'Sec-Fetch-Dest: document' \\\n" + + " -H 'Sec-Fetch-Mode: navigate' \\\n" + + " -H 'Sec-Fetch-Site: same-origin' \\\n" + + " -H 'Sec-Fetch-User: ?1' \\\n" + + " -H 'Upgrade-Insecure-Requests: 1' \\\n" + + " -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' \\\n" + + " -H 'sec-ch-ua: \"Not)A;Brand\";v=\"99\", \"Google Chrome\";v=\"127\", \"Chromium\";v=\"127\"' \\\n" + + " -H 'sec-ch-ua-mobile: ?0' \\\n" + + " -H 'sec-ch-ua-platform: \"macOS\"'"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + + curl = "curl 'https://127.0.0.1:8081/api/definition/page' \\\n" + + " -H 'Accept: application/json, text/plain, */*' \\\n" + + " -H 'Accept-Language: zh-CN' \\\n" + + " -H 'CSRF-TOKEN: 1234454351313131431' \\\n" + + " -H 'Connection: keep-alive' \\\n" + + " -H 'Content-Type: application/json;charset=UTF-8' \\\n" + + " -H 'ORGANIZATION: 100001' \\\n" + + " -H 'Origin: http://127.0.0.1:8081' \\\n" + + " -H 'PROJECT: 100001100001' \\\n" + + " -H 'Referer: http://127.0.0.1:8081/' \\\n" + + " -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' \\\n" + + " -H 'X-AUTH-TOKEN: 45fdgsrgdsg-2baf-40bc-98ba-5dsg15s1fg' \\\n" + + " --data-raw '{current:1,\"pageSize\":10,\"sort\":{},\"keyword\":\"\",\"combine\":{},\"searchMode\":\"AND\",\"projectId\":\"100001100001\",\"moduleIds\":[],\"protocols\":[\"HTTP\"],\"filter\":{\"status\":[],\"method\":[],\"priority\":[]},\"excludeIds\":[\"123456783242123\",\"\",\"\"]}' \\\n" + + " --insecure"; + request.setCurl(curl); + this.requestPost(IMPORT_CURL, request); + + curl = "curl -X POST -H 'Content-Type: application/json' --data-urlencode '{\"key\":\"value\"}' https://example.com/post"; + request.setCurl(curl); + this.requestPostWithOk(IMPORT_CURL, request); + } + + } \ No newline at end of file