Merge branch 'master' of https://github.com/metersphere/metersphere
This commit is contained in:
commit
8fd1ddbf2b
|
@ -40,6 +40,8 @@ public class MsJSR223Processor extends MsTestElement {
|
||||||
if (StringUtils.isNotEmpty(name) && !config.isOperating()) {
|
if (StringUtils.isNotEmpty(name) && !config.isOperating()) {
|
||||||
processor.setName(this.getName() + "<->" + name);
|
processor.setName(this.getName() + "<->" + name);
|
||||||
}
|
}
|
||||||
|
processor.setProperty("MS-ID", this.getId());
|
||||||
|
|
||||||
processor.setProperty(TestElement.TEST_CLASS, JSR223Sampler.class.getName());
|
processor.setProperty(TestElement.TEST_CLASS, JSR223Sampler.class.getName());
|
||||||
processor.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
|
processor.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
|
||||||
processor.setProperty("cacheKey", "true");
|
processor.setProperty("cacheKey", "true");
|
||||||
|
|
|
@ -82,6 +82,7 @@ public class MsDubboSampler extends MsTestElement {
|
||||||
}
|
}
|
||||||
sampler.setProperty(TestElement.TEST_CLASS, DubboSample.class.getName());
|
sampler.setProperty(TestElement.TEST_CLASS, DubboSample.class.getName());
|
||||||
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("DubboSampleGui"));
|
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("DubboSampleGui"));
|
||||||
|
sampler.setProperty("MS-ID", this.getId());
|
||||||
|
|
||||||
sampler.addTestElement(configCenter(this.getConfigCenter()));
|
sampler.addTestElement(configCenter(this.getConfigCenter()));
|
||||||
sampler.addTestElement(registryCenter(this.getRegistryCenter()));
|
sampler.addTestElement(registryCenter(this.getRegistryCenter()));
|
||||||
|
|
|
@ -106,6 +106,7 @@ public class MsHTTPSamplerProxy extends MsTestElement {
|
||||||
}
|
}
|
||||||
sampler.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
|
sampler.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
|
||||||
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("HttpTestSampleGui"));
|
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("HttpTestSampleGui"));
|
||||||
|
sampler.setProperty("MS-ID", this.getId());
|
||||||
sampler.setMethod(this.getMethod());
|
sampler.setMethod(this.getMethod());
|
||||||
sampler.setContentEncoding("UTF-8");
|
sampler.setContentEncoding("UTF-8");
|
||||||
sampler.setConnectTimeout(this.getConnectTimeout() == null ? "6000" : this.getConnectTimeout());
|
sampler.setConnectTimeout(this.getConnectTimeout() == null ? "6000" : this.getConnectTimeout());
|
||||||
|
|
|
@ -119,6 +119,8 @@ public class MsJDBCSampler extends MsTestElement {
|
||||||
}
|
}
|
||||||
sampler.setProperty(TestElement.TEST_CLASS, JDBCSampler.class.getName());
|
sampler.setProperty(TestElement.TEST_CLASS, JDBCSampler.class.getName());
|
||||||
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
|
sampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
|
||||||
|
sampler.setProperty("MS-ID", this.getId());
|
||||||
|
|
||||||
// request.getDataSource() 是ID,需要转换为Name
|
// request.getDataSource() 是ID,需要转换为Name
|
||||||
sampler.setProperty("dataSource", this.dataSource.getName());
|
sampler.setProperty("dataSource", this.dataSource.getName());
|
||||||
sampler.setProperty("query", this.getQuery());
|
sampler.setProperty("query", this.getQuery());
|
||||||
|
|
|
@ -117,6 +117,7 @@ public class MsTCPSampler extends MsTestElement {
|
||||||
if (StringUtils.isNotEmpty(name) && !config.isOperating()) {
|
if (StringUtils.isNotEmpty(name) && !config.isOperating()) {
|
||||||
tcpSampler.setName(this.getName() + "<->" + name);
|
tcpSampler.setName(this.getName() + "<->" + name);
|
||||||
}
|
}
|
||||||
|
tcpSampler.setProperty("MS-ID", this.getId());
|
||||||
|
|
||||||
tcpSampler.setProperty(TestElement.TEST_CLASS, TCPSampler.class.getName());
|
tcpSampler.setProperty(TestElement.TEST_CLASS, TCPSampler.class.getName());
|
||||||
tcpSampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TCPSamplerGui"));
|
tcpSampler.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TCPSamplerGui"));
|
||||||
|
|
|
@ -239,7 +239,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
||||||
LogUtil.error(e.getMessage(), e);
|
LogUtil.error(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendTask(report, reportUrl, testResult);
|
sendTask(report, reportUrl, testResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sendTask(ApiTestReport report, String reportUrl, TestResult testResult) {
|
private static void sendTask(ApiTestReport report, String reportUrl, TestResult testResult) {
|
||||||
|
@ -303,6 +303,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
|
||||||
|
|
||||||
private RequestResult getRequestResult(SampleResult result) {
|
private RequestResult getRequestResult(SampleResult result) {
|
||||||
RequestResult requestResult = new RequestResult();
|
RequestResult requestResult = new RequestResult();
|
||||||
|
requestResult.setId(result.getSamplerId());
|
||||||
requestResult.setName(result.getSampleLabel());
|
requestResult.setName(result.getSampleLabel());
|
||||||
requestResult.setUrl(result.getUrlAsString());
|
requestResult.setUrl(result.getUrlAsString());
|
||||||
requestResult.setMethod(getMethod(result));
|
requestResult.setMethod(getMethod(result));
|
||||||
|
|
|
@ -7,6 +7,8 @@ import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class RequestResult {
|
public class RequestResult {
|
||||||
|
// 请求ID
|
||||||
|
private String id;
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
package io.metersphere.commons.user;
|
package io.metersphere.commons.user;
|
||||||
|
|
||||||
|
import io.metersphere.commons.utils.CodingUtil;
|
||||||
import io.metersphere.dto.UserDTO;
|
import io.metersphere.dto.UserDTO;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
public class SessionUser extends UserDTO implements Serializable {
|
public class SessionUser extends UserDTO implements Serializable {
|
||||||
|
public static final String secret = "9a9rdqPlTqhpZzkq";
|
||||||
|
public static final String iv = "1Av7hf9PgHusUHRm";
|
||||||
|
|
||||||
private static final long serialVersionUID = -7149638440406959033L;
|
private static final long serialVersionUID = -7149638440406959033L;
|
||||||
|
private String csrfToken;
|
||||||
|
|
||||||
|
private SessionUser() {
|
||||||
|
}
|
||||||
|
|
||||||
public static SessionUser fromUser(UserDTO user) {
|
public static SessionUser fromUser(UserDTO user) {
|
||||||
SessionUser sessionUser = new SessionUser();
|
SessionUser sessionUser = new SessionUser();
|
||||||
BeanUtils.copyProperties(user, sessionUser);
|
BeanUtils.copyProperties(user, sessionUser);
|
||||||
|
|
||||||
|
List<String> infos = Arrays.asList(user.getId(), RandomStringUtils.random(6), "" + System.currentTimeMillis());
|
||||||
|
sessionUser.csrfToken = CodingUtil.aesEncrypt(StringUtils.join(infos, "|"), secret, iv);
|
||||||
return sessionUser;
|
return sessionUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,14 +62,14 @@ public class SessionUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrentWorkspaceId() {
|
public static String getCurrentWorkspaceId() {
|
||||||
return Optional.ofNullable(getUser()).orElse(new SessionUser()).getLastWorkspaceId();
|
return getUser().getLastWorkspaceId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrentOrganizationId() {
|
public static String getCurrentOrganizationId() {
|
||||||
return Optional.ofNullable(getUser()).orElse(new SessionUser()).getLastOrganizationId();
|
return getUser().getLastOrganizationId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrentProjectId() {
|
public static String getCurrentProjectId() {
|
||||||
return Optional.ofNullable(getUser()).orElse(new SessionUser()).getLastProjectId();
|
return getUser().getLastProjectId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,10 @@ public class ShiroUtils {
|
||||||
// filterChainDefinitionMap.put("/document/**", "anon");
|
// filterChainDefinitionMap.put("/document/**", "anon");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void ignoreCsrfFilter(Map<String, String> filterChainDefinitionMap) {
|
||||||
|
filterChainDefinitionMap.put("/", "apikey, authc"); // 跳转到 / 不用校验 csrf
|
||||||
|
}
|
||||||
|
|
||||||
public static Cookie getSessionIdCookie(){
|
public static Cookie getSessionIdCookie(){
|
||||||
SimpleCookie sessionIdCookie = new SimpleCookie();
|
SimpleCookie sessionIdCookie = new SimpleCookie();
|
||||||
sessionIdCookie.setPath("/");
|
sessionIdCookie.setPath("/");
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.metersphere.config;
|
||||||
|
|
||||||
import io.metersphere.commons.utils.ShiroUtils;
|
import io.metersphere.commons.utils.ShiroUtils;
|
||||||
import io.metersphere.security.ApiKeyFilter;
|
import io.metersphere.security.ApiKeyFilter;
|
||||||
|
import io.metersphere.security.CsrfFilter;
|
||||||
import io.metersphere.security.UserModularRealmAuthenticator;
|
import io.metersphere.security.UserModularRealmAuthenticator;
|
||||||
import io.metersphere.security.realm.LdapRealm;
|
import io.metersphere.security.realm.LdapRealm;
|
||||||
import io.metersphere.security.realm.ShiroDBRealm;
|
import io.metersphere.security.realm.ShiroDBRealm;
|
||||||
|
@ -44,10 +45,14 @@ public class ShiroConfig implements EnvironmentAware {
|
||||||
shiroFilterFactoryBean.setSuccessUrl("/");
|
shiroFilterFactoryBean.setSuccessUrl("/");
|
||||||
|
|
||||||
shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());
|
shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());
|
||||||
|
shiroFilterFactoryBean.getFilters().put("csrf", new CsrfFilter());
|
||||||
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
|
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
|
||||||
|
|
||||||
ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);
|
ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);
|
||||||
|
|
||||||
filterChainDefinitionMap.put("/**", "apikey, authc");
|
ShiroUtils.ignoreCsrfFilter(filterChainDefinitionMap);
|
||||||
|
|
||||||
|
filterChainDefinitionMap.put("/**", "apikey, csrf, authc");
|
||||||
return shiroFilterFactoryBean;
|
return shiroFilterFactoryBean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package io.metersphere.controller;
|
package io.metersphere.controller;
|
||||||
|
|
||||||
import io.metersphere.commons.constants.SsoMode;
|
|
||||||
import io.metersphere.commons.constants.UserSource;
|
import io.metersphere.commons.constants.UserSource;
|
||||||
import io.metersphere.commons.user.SessionUser;
|
import io.metersphere.commons.user.SessionUser;
|
||||||
import io.metersphere.commons.utils.SessionUtils;
|
import io.metersphere.commons.utils.SessionUtils;
|
||||||
|
@ -10,7 +9,6 @@ import io.metersphere.service.UserService;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.springframework.context.i18n.LocaleContextHolder;
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.core.env.Environment;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@ -24,8 +22,6 @@ public class LoginController {
|
||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
@Resource
|
@Resource
|
||||||
private Environment env;
|
|
||||||
@Resource
|
|
||||||
private BaseDisplayService baseDisplayService;
|
private BaseDisplayService baseDisplayService;
|
||||||
|
|
||||||
@GetMapping(value = "/isLogin")
|
@GetMapping(value = "/isLogin")
|
||||||
|
@ -37,10 +33,6 @@ public class LoginController {
|
||||||
}
|
}
|
||||||
return ResultHolder.success(user);
|
return ResultHolder.success(user);
|
||||||
}
|
}
|
||||||
String ssoMode = env.getProperty("sso.mode");
|
|
||||||
if (ssoMode != null && StringUtils.equalsIgnoreCase(SsoMode.CAS.name(), ssoMode)) {
|
|
||||||
return ResultHolder.error("sso");
|
|
||||||
}
|
|
||||||
return ResultHolder.error("");
|
return ResultHolder.error("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
package io.metersphere.controller;
|
package io.metersphere.controller;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSONObject;
|
|
||||||
import com.github.pagehelper.Page;
|
import com.github.pagehelper.Page;
|
||||||
import com.github.pagehelper.PageHelper;
|
import com.github.pagehelper.PageHelper;
|
||||||
import io.metersphere.base.domain.Organization;
|
|
||||||
import io.metersphere.base.domain.User;
|
import io.metersphere.base.domain.User;
|
||||||
import io.metersphere.base.domain.Workspace;
|
|
||||||
import io.metersphere.commons.constants.RoleConstants;
|
import io.metersphere.commons.constants.RoleConstants;
|
||||||
import io.metersphere.commons.exception.MSException;
|
import io.metersphere.commons.exception.MSException;
|
||||||
import io.metersphere.commons.user.SessionUser;
|
import io.metersphere.commons.user.SessionUser;
|
||||||
|
@ -29,7 +26,6 @@ import io.metersphere.service.WorkspaceService;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.Logical;
|
import org.apache.shiro.authz.annotation.Logical;
|
||||||
import org.apache.shiro.authz.annotation.RequiresRoles;
|
import org.apache.shiro.authz.annotation.RequiresRoles;
|
||||||
import org.checkerframework.checker.units.qual.C;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,7 @@ public class EngineContext {
|
||||||
private String reportId;
|
private String reportId;
|
||||||
private Integer resourceIndex;
|
private Integer resourceIndex;
|
||||||
private Map<String, Object> properties = new HashMap<>();
|
private Map<String, Object> properties = new HashMap<>();
|
||||||
private Map<String, String> testData = new HashMap<>();
|
private Map<String, byte[]> testResourceFiles = new HashMap<>();
|
||||||
private Map<String, byte[]> testJars = new HashMap<>();
|
|
||||||
|
|
||||||
public String getTestId() {
|
public String getTestId() {
|
||||||
return testId;
|
return testId;
|
||||||
|
@ -69,14 +68,6 @@ public class EngineContext {
|
||||||
this.fileType = fileType;
|
this.fileType = fileType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getTestData() {
|
|
||||||
return testData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTestData(Map<String, String> testData) {
|
|
||||||
this.testData = testData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getResourcePoolId() {
|
public String getResourcePoolId() {
|
||||||
return resourcePoolId;
|
return resourcePoolId;
|
||||||
}
|
}
|
||||||
|
@ -111,11 +102,11 @@ public class EngineContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Map<String, byte[]> getTestJars() {
|
public Map<String, byte[]> getTestResourceFiles() {
|
||||||
return testJars;
|
return testResourceFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTestJars(Map<String, byte[]> testJars) {
|
public void setTestResourceFiles(Map<String, byte[]> testResourceFiles) {
|
||||||
this.testJars = testJars;
|
this.testResourceFiles = testResourceFiles;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,8 +92,7 @@ public class EngineFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FileMetadata> jmxFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList());
|
List<FileMetadata> jmxFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList());
|
||||||
List<FileMetadata> csvFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.CSV.name())).collect(Collectors.toList());
|
List<FileMetadata> resourceFiles = fileMetadataList.stream().filter(f -> !StringUtils.equalsIgnoreCase(f.getType(), FileType.JMX.name())).collect(Collectors.toList());
|
||||||
List<FileMetadata> jarFiles = fileMetadataList.stream().filter(f -> StringUtils.equalsIgnoreCase(f.getType(), FileType.JAR.name())).collect(Collectors.toList());
|
|
||||||
// 合并上传的jmx
|
// 合并上传的jmx
|
||||||
byte[] jmxBytes = mergeJmx(jmxFiles);
|
byte[] jmxBytes = mergeJmx(jmxFiles);
|
||||||
final EngineContext engineContext = new EngineContext();
|
final EngineContext engineContext = new EngineContext();
|
||||||
|
@ -156,22 +155,13 @@ public class EngineFactory {
|
||||||
MSException.throwException(e);
|
MSException.throwException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CollectionUtils.isNotEmpty(csvFiles)) {
|
if (CollectionUtils.isNotEmpty(resourceFiles)) {
|
||||||
Map<String, String> data = new HashMap<>();
|
|
||||||
csvFiles.forEach(cf -> {
|
|
||||||
FileContent csvContent = fileService.getFileContent(cf.getId());
|
|
||||||
data.put(cf.getName(), new String(csvContent.getFile()));
|
|
||||||
});
|
|
||||||
engineContext.setTestData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CollectionUtils.isNotEmpty(jarFiles)) {
|
|
||||||
Map<String, byte[]> data = new HashMap<>();
|
Map<String, byte[]> data = new HashMap<>();
|
||||||
jarFiles.forEach(jf -> {
|
resourceFiles.forEach(cf -> {
|
||||||
FileContent content = fileService.getFileContent(jf.getId());
|
FileContent csvContent = fileService.getFileContent(cf.getId());
|
||||||
data.put(jf.getName(), content.getFile());
|
data.put(cf.getName(), csvContent.getFile());
|
||||||
});
|
});
|
||||||
engineContext.setTestJars(data);
|
engineContext.setTestResourceFiles(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return engineContext;
|
return engineContext;
|
||||||
|
|
|
@ -51,17 +51,9 @@ public class JmeterFileService {
|
||||||
|
|
||||||
// 每个测试生成一个文件夹
|
// 每个测试生成一个文件夹
|
||||||
files.put(fileName, context.getContent().getBytes(StandardCharsets.UTF_8));
|
files.put(fileName, context.getContent().getBytes(StandardCharsets.UTF_8));
|
||||||
// 保存测试数据文件
|
|
||||||
Map<String, String> testData = context.getTestData();
|
|
||||||
if (!CollectionUtils.isEmpty(testData)) {
|
|
||||||
for (String k : testData.keySet()) {
|
|
||||||
String v = testData.get(k);
|
|
||||||
files.put(k, v.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存 byte[] jar
|
// 保存 byte[]
|
||||||
Map<String, byte[]> jarFiles = context.getTestJars();
|
Map<String, byte[]> jarFiles = context.getTestResourceFiles();
|
||||||
if (!CollectionUtils.isEmpty(jarFiles)) {
|
if (!CollectionUtils.isEmpty(jarFiles)) {
|
||||||
for (String k : jarFiles.keySet()) {
|
for (String k : jarFiles.keySet()) {
|
||||||
byte[] v = jarFiles.get(k);
|
byte[] v = jarFiles.get(k);
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package io.metersphere.security;
|
||||||
|
|
||||||
|
import io.metersphere.commons.user.SessionUser;
|
||||||
|
import io.metersphere.commons.utils.CodingUtil;
|
||||||
|
import io.metersphere.commons.utils.CommonBeanFactory;
|
||||||
|
import io.metersphere.commons.utils.SessionUtils;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.web.filter.authc.AnonymousFilter;
|
||||||
|
import org.apache.shiro.web.util.WebUtils;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class CsrfFilter extends AnonymousFilter {
|
||||||
|
private static final String TOKEN_NAME = "CSRF-TOKEN";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||||
|
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
|
||||||
|
|
||||||
|
if (!SecurityUtils.getSubject().isAuthenticated()) {
|
||||||
|
((HttpServletResponse) response).setHeader("Authentication-Status", "invalid");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// api 过来的请求
|
||||||
|
if (ApiKeyHandler.isApiKeyCall(WebUtils.toHttp(request))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// websocket 不需要csrf
|
||||||
|
String websocketKey = httpServletRequest.getHeader("Sec-WebSocket-Key");
|
||||||
|
if (StringUtils.isNotBlank(websocketKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求头取出的token value
|
||||||
|
String csrfToken = httpServletRequest.getHeader(TOKEN_NAME);
|
||||||
|
// 校验 token
|
||||||
|
validateToken(csrfToken);
|
||||||
|
// 校验 referer
|
||||||
|
validateReferer(httpServletRequest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateReferer(HttpServletRequest request) {
|
||||||
|
Environment env = CommonBeanFactory.getBean(Environment.class);
|
||||||
|
String domains = env.getProperty("referer.urls");
|
||||||
|
if (StringUtils.isBlank(domains)) {
|
||||||
|
// 没有配置不校验
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] split = StringUtils.split(domains, ",");
|
||||||
|
String referer = request.getHeader(HttpHeaders.REFERER);
|
||||||
|
if (split != null) {
|
||||||
|
if (!ArrayUtils.contains(split, referer)) {
|
||||||
|
throw new RuntimeException("csrf error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateToken(String csrfToken) {
|
||||||
|
if (StringUtils.isBlank(csrfToken)) {
|
||||||
|
throw new RuntimeException("csrf token is empty");
|
||||||
|
}
|
||||||
|
csrfToken = CodingUtil.aesDecrypt(csrfToken, SessionUser.secret, SessionUser.iv);
|
||||||
|
|
||||||
|
String[] signatureArray = StringUtils.split(StringUtils.trimToNull(csrfToken), "|");
|
||||||
|
if (signatureArray.length != 3) {
|
||||||
|
throw new RuntimeException("invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
long signatureTime;
|
||||||
|
try {
|
||||||
|
signatureTime = Long.parseLong(signatureArray[2]);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
Environment env = CommonBeanFactory.getBean(Environment.class);
|
||||||
|
long timeout = env.getProperty("session.timeout", Long.class, 43200L);
|
||||||
|
if (Math.abs(System.currentTimeMillis() - signatureTime) > timeout * 1000) {
|
||||||
|
throw new RuntimeException("expired token");
|
||||||
|
}
|
||||||
|
if (!StringUtils.equals(SessionUtils.getUserId(), signatureArray[0])) {
|
||||||
|
throw new RuntimeException("Please check csrf token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -30,6 +30,7 @@ import MsUser from "./components/common/head/HeaderUser";
|
||||||
import MsHeaderOrgWs from "./components/common/head/HeaderOrgWs";
|
import MsHeaderOrgWs from "./components/common/head/HeaderOrgWs";
|
||||||
import MsLanguageSwitch from "./components/common/head/LanguageSwitch";
|
import MsLanguageSwitch from "./components/common/head/LanguageSwitch";
|
||||||
import {saveLocalStorage} from "@/common/js/utils";
|
import {saveLocalStorage} from "@/common/js/utils";
|
||||||
|
import {registerRequestHeaders} from "@/common/js/ajax";
|
||||||
|
|
||||||
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
|
const requireComponent = require.context('@/business/components/xpack/', true, /\.vue$/);
|
||||||
const header = requireComponent.keys().length > 0 ? requireComponent("./license/LicenseMessage.vue") : {};
|
const header = requireComponent.keys().length > 0 ? requireComponent("./license/LicenseMessage.vue") : {};
|
||||||
|
@ -53,6 +54,7 @@ export default {
|
||||||
window.addEventListener("beforeunload", () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
localStorage.setItem("store", JSON.stringify(this.$store.state))
|
localStorage.setItem("store", JSON.stringify(this.$store.state))
|
||||||
})
|
})
|
||||||
|
registerRequestHeaders();
|
||||||
},
|
},
|
||||||
beforeCreate() {
|
beforeCreate() {
|
||||||
this.$get("/isLogin").then(response => {
|
this.$get("/isLogin").then(response => {
|
||||||
|
|
|
@ -100,6 +100,19 @@
|
||||||
active() {
|
active() {
|
||||||
this.isActive = !this.isActive;
|
this.isActive = !this.isActive;
|
||||||
},
|
},
|
||||||
|
formatResult(res) {
|
||||||
|
let resMap = new Map;
|
||||||
|
if (res && res.scenarios) {
|
||||||
|
res.scenarios.forEach(item => {
|
||||||
|
if (item && item.requestResults) {
|
||||||
|
item.requestResults.forEach(req => {
|
||||||
|
resMap.set(req.id, req);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.$emit('refresh', resMap);
|
||||||
|
},
|
||||||
getReport() {
|
getReport() {
|
||||||
this.init();
|
this.init();
|
||||||
if (this.reportId) {
|
if (this.reportId) {
|
||||||
|
@ -113,7 +126,7 @@
|
||||||
if (!this.content) {
|
if (!this.content) {
|
||||||
this.content = {scenarios: []};
|
this.content = {scenarios: []};
|
||||||
}
|
}
|
||||||
this.$emit('refresh');
|
this.formatResult(this.content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,7 +184,7 @@
|
||||||
<!-- 调试结果 -->
|
<!-- 调试结果 -->
|
||||||
<el-drawer v-if="type!=='detail'" :visible.sync="debugVisible" :destroy-on-close="true" direction="ltr"
|
<el-drawer v-if="type!=='detail'" :visible.sync="debugVisible" :destroy-on-close="true" direction="ltr"
|
||||||
:withHeader="true" :modal="false" size="90%">
|
:withHeader="true" :modal="false" size="90%">
|
||||||
<ms-api-report-detail :report-id="reportId" :debug="true" :currentProjectId="projectId"/>
|
<ms-api-report-detail :report-id="reportId" :debug="true" :currentProjectId="projectId" @refresh="detailRefresh"/>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<!--场景公共参数-->
|
<!--场景公共参数-->
|
||||||
|
@ -291,7 +291,8 @@
|
||||||
response: {},
|
response: {},
|
||||||
projectIds: new Set,
|
projectIds: new Set,
|
||||||
projectEnvMap: new Map,
|
projectEnvMap: new Map,
|
||||||
projectList: []
|
projectList: [],
|
||||||
|
debugResult: new Map,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -548,21 +549,32 @@
|
||||||
if (arr[i].hashTree != undefined && arr[i].hashTree.length > 0) {
|
if (arr[i].hashTree != undefined && arr[i].hashTree.length > 0) {
|
||||||
this.recursiveSorting(arr[i].hashTree);
|
this.recursiveSorting(arr[i].hashTree);
|
||||||
}
|
}
|
||||||
|
// 添加debug结果
|
||||||
|
if (this.debugResult && this.debugResult.get(arr[i].id)) {
|
||||||
|
arr[i].requestResult = this.debugResult.get(arr[i].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort() {
|
sort() {
|
||||||
for (let i in this.scenarioDefinition) {
|
for (let i in this.scenarioDefinition) {
|
||||||
|
// 排序
|
||||||
this.scenarioDefinition[i].index = Number(i) + 1;
|
this.scenarioDefinition[i].index = Number(i) + 1;
|
||||||
|
// 设置循环控制
|
||||||
if (this.scenarioDefinition[i].type === ELEMENT_TYPE.LoopController && this.scenarioDefinition[i].hashTree
|
if (this.scenarioDefinition[i].type === ELEMENT_TYPE.LoopController && this.scenarioDefinition[i].hashTree
|
||||||
&& this.scenarioDefinition[i].hashTree.length > 1) {
|
&& this.scenarioDefinition[i].hashTree.length > 1) {
|
||||||
this.scenarioDefinition[i].countController.proceed = true;
|
this.scenarioDefinition[i].countController.proceed = true;
|
||||||
}
|
}
|
||||||
|
// 设置项目ID
|
||||||
if (!this.scenarioDefinition[i].projectId) {
|
if (!this.scenarioDefinition[i].projectId) {
|
||||||
this.scenarioDefinition[i].projectId = getCurrentProjectID();
|
this.scenarioDefinition[i].projectId = getCurrentProjectID();
|
||||||
}
|
}
|
||||||
if (this.scenarioDefinition[i].hashTree != undefined && this.scenarioDefinition[i].hashTree.length > 0) {
|
if (this.scenarioDefinition[i].hashTree != undefined && this.scenarioDefinition[i].hashTree.length > 0) {
|
||||||
this.recursiveSorting(this.scenarioDefinition[i].hashTree);
|
this.recursiveSorting(this.scenarioDefinition[i].hashTree);
|
||||||
}
|
}
|
||||||
|
// 添加debug结果
|
||||||
|
if (this.debugResult && this.debugResult.get(this.scenarioDefinition[i].id)) {
|
||||||
|
this.scenarioDefinition[i].requestResult = this.debugResult.get(this.scenarioDefinition[i].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addCustomizeApi(request) {
|
addCustomizeApi(request) {
|
||||||
|
@ -576,6 +588,7 @@
|
||||||
this.customizeRequest = {};
|
this.customizeRequest = {};
|
||||||
this.sort();
|
this.sort();
|
||||||
this.reload();
|
this.reload();
|
||||||
|
this.initProjectIds();
|
||||||
},
|
},
|
||||||
addScenario(arr) {
|
addScenario(arr) {
|
||||||
if (arr && arr.length > 0) {
|
if (arr && arr.length > 0) {
|
||||||
|
@ -1025,6 +1038,11 @@
|
||||||
arr.forEach(a => this.projectIds.add(a));
|
arr.forEach(a => this.projectIds.add(a));
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
detailRefresh(result) {
|
||||||
|
// 把执行结果分发给各个请求
|
||||||
|
this.debugResult = result;
|
||||||
|
this.sort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,17 @@ export default {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
sign = false;
|
// 如果有环境,检查环境
|
||||||
|
if (this.envMap && this.envMap.size > 0) {
|
||||||
|
this.projectIds.forEach(id => {
|
||||||
|
if (!this.envMap.get(id)) {
|
||||||
|
sign = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
sign = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sign) {
|
if (!sign) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -117,6 +117,36 @@
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form :inline="true">
|
||||||
|
<el-form-item>
|
||||||
|
<div>
|
||||||
|
{{ $t('load_test.granularity') }}
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
width="400"
|
||||||
|
trigger="hover">
|
||||||
|
<el-table :data="granularityData">
|
||||||
|
<el-table-column property="start" :label="$t('load_test.duration')">
|
||||||
|
<template v-slot:default="scope">
|
||||||
|
<span>{{ scope.row.start }} - {{ scope.row.end }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column property="granularity" :label="$t('load_test.granularity')"/>
|
||||||
|
</el-table>
|
||||||
|
<i slot="reference" class="el-icon-info pointer"/>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-select v-model="granularity" :placeholder="$t('commons.please_select')" size="mini" clearable>
|
||||||
|
<el-option v-for="op in granularityData" :key="op.granularity" :label="op.granularity" :value="op.granularity"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -134,6 +164,18 @@ export default {
|
||||||
domains: [],
|
domains: [],
|
||||||
params: [],
|
params: [],
|
||||||
statusCodeStr: '',
|
statusCodeStr: '',
|
||||||
|
granularity: undefined,
|
||||||
|
granularityData: [
|
||||||
|
{start: 0, end: 100, granularity: 1},
|
||||||
|
{start: 101, end: 500, granularity: 5},
|
||||||
|
{start: 501, end: 1000, granularity: 10},
|
||||||
|
{start: 1001, end: 3000, granularity: 30},
|
||||||
|
{start: 3001, end: 6000, granularity: 60},
|
||||||
|
{start: 6001, end: 30000, granularity: 300},
|
||||||
|
{start: 30001, end: 60000, granularity: 600},
|
||||||
|
{start: 60001, end: 180000, granularity: 1800},
|
||||||
|
{start: 180001, end: 360000, granularity: 3600},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -166,6 +208,7 @@ export default {
|
||||||
this.statusCodeStr = this.statusCode.join(',');
|
this.statusCodeStr = this.statusCode.join(',');
|
||||||
this.domains = data.domains || [];
|
this.domains = data.domains || [];
|
||||||
this.params = data.params || [];
|
this.params = data.params || [];
|
||||||
|
this.granularity = data.granularity;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -252,6 +295,7 @@ export default {
|
||||||
statusCode: statusCode,
|
statusCode: statusCode,
|
||||||
params: this.params,
|
params: this.params,
|
||||||
domains: this.domains,
|
domains: this.domains,
|
||||||
|
granularity: this.granularity,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -287,4 +331,8 @@ export default {
|
||||||
align: center;
|
align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<el-row type="flex" justify="start" align="middle">
|
<el-row type="flex" justify="start" align="middle">
|
||||||
<el-upload
|
<el-upload
|
||||||
style="padding-right: 10px;"
|
style="padding-right: 10px;"
|
||||||
accept=".jar,.csv"
|
accept=".jar,.csv,.json,.pdf,.jpg,.png,.jpeg,.doc,.docx,.xlsx"
|
||||||
action=""
|
action=""
|
||||||
:limit="fileNumLimit"
|
:limit="fileNumLimit"
|
||||||
multiple
|
multiple
|
||||||
|
@ -170,7 +170,7 @@ export default {
|
||||||
fileList: [],
|
fileList: [],
|
||||||
tableData: [],
|
tableData: [],
|
||||||
uploadList: [],
|
uploadList: [],
|
||||||
metadataIdList:[],
|
metadataIdList: [],
|
||||||
fileNumLimit: 10,
|
fileNumLimit: 10,
|
||||||
threadGroups: [],
|
threadGroups: [],
|
||||||
loadFileVisible: false,
|
loadFileVisible: false,
|
||||||
|
@ -276,7 +276,10 @@ export default {
|
||||||
let self = this;
|
let self = this;
|
||||||
let file = uploadResources.file;
|
let file = uploadResources.file;
|
||||||
self.uploadList.push(file);
|
self.uploadList.push(file);
|
||||||
|
let type = file.name.substring(file.name.lastIndexOf(".") + 1);
|
||||||
|
if (type.toLowerCase() !== 'jmx') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let jmxReader = new FileReader();
|
let jmxReader = new FileReader();
|
||||||
jmxReader.onload = (event) => {
|
jmxReader.onload = (event) => {
|
||||||
self.threadGroups = self.threadGroups.concat(findThreadGroup(event.target.result, file.name));
|
self.threadGroups = self.threadGroups.concat(findThreadGroup(event.target.result, file.name));
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 360d7214d15951ae11b3973add795305a5c3d035
|
Subproject commit 290ffd9eb52b1a42243adb35ac3ee61f8295bb8f
|
|
@ -22,7 +22,6 @@ import {left2RightDrag, bottom2TopDrag, right2LeftDrag} from "../common/js/direc
|
||||||
import JsonSchemaEditor from './components/common/json-schema/schema/index';
|
import JsonSchemaEditor from './components/common/json-schema/schema/index';
|
||||||
import JSONPathPicker from 'vue-jsonpath-picker';
|
import JSONPathPicker from 'vue-jsonpath-picker';
|
||||||
import VueClipboard from 'vue-clipboard2'
|
import VueClipboard from 'vue-clipboard2'
|
||||||
import 'default-passive-events'
|
|
||||||
Vue.use(JsonSchemaEditor);
|
Vue.use(JsonSchemaEditor);
|
||||||
import VuePapaParse from 'vue-papa-parse'
|
import VuePapaParse from 'vue-papa-parse'
|
||||||
Vue.use(VuePapaParse)
|
Vue.use(VuePapaParse)
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import {Message, MessageBox} from 'element-ui';
|
import {Message, MessageBox} from 'element-ui';
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18n from '../../i18n/i18n'
|
import i18n from '../../i18n/i18n'
|
||||||
|
import {TokenKey} from "@/common/js/constants";
|
||||||
|
|
||||||
|
export function registerRequestHeaders() {
|
||||||
|
axios.interceptors.request.use(config => {
|
||||||
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
if (user && user.csrfToken) {
|
||||||
|
config.headers['CSRF-TOKEN'] = user.csrfToken;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
|
|
||||||
// 登入请求不重定向
|
// 登入请求不重定向
|
||||||
let unRedirectUrls = new Set(['signin','ldap/signin','/signin', '/ldap/signin']);
|
let unRedirectUrls = new Set(['signin', 'ldap/signin', '/signin', '/ldap/signin']);
|
||||||
|
|
||||||
if (!axios) {
|
if (!axios) {
|
||||||
window.console.error('You have to install axios');
|
window.console.error('You have to install axios');
|
||||||
|
|
|
@ -478,7 +478,8 @@ export default {
|
||||||
delete_file: "The file already exists, please delete the file with the same name first!",
|
delete_file: "The file already exists, please delete the file with the same name first!",
|
||||||
thread_num: 'Concurrent users:',
|
thread_num: 'Concurrent users:',
|
||||||
input_thread_num: 'Please enter the number of threads',
|
input_thread_num: 'Please enter the number of threads',
|
||||||
duration: 'Duration time (seconds):',
|
duration: 'Duration time (seconds)',
|
||||||
|
granularity: 'Aggregation time (seconds)',
|
||||||
input_duration: 'Please enter a duration',
|
input_duration: 'Please enter a duration',
|
||||||
rps_limit: 'RPS Limit:',
|
rps_limit: 'RPS Limit:',
|
||||||
input_rps_limit: 'Please enter a limit',
|
input_rps_limit: 'Please enter a limit',
|
||||||
|
|
|
@ -475,7 +475,8 @@ export default {
|
||||||
delete_file: "文件已存在,请先删除同名文件!",
|
delete_file: "文件已存在,请先删除同名文件!",
|
||||||
thread_num: '并发用户数:',
|
thread_num: '并发用户数:',
|
||||||
input_thread_num: '请输入线程数',
|
input_thread_num: '请输入线程数',
|
||||||
duration: '压测时长(秒):',
|
duration: '压测时长(秒)',
|
||||||
|
granularity: '聚合时间(秒)',
|
||||||
input_duration: '请输入时长',
|
input_duration: '请输入时长',
|
||||||
rps_limit: 'RPS上限:',
|
rps_limit: 'RPS上限:',
|
||||||
input_rps_limit: '请输入限制',
|
input_rps_limit: '请输入限制',
|
||||||
|
|
|
@ -475,7 +475,8 @@ export default {
|
||||||
delete_file: "文件已存在,請先刪除同名文件!",
|
delete_file: "文件已存在,請先刪除同名文件!",
|
||||||
thread_num: '並發用戶數:',
|
thread_num: '並發用戶數:',
|
||||||
input_thread_num: '請輸入線程數',
|
input_thread_num: '請輸入線程數',
|
||||||
duration: '壓測時長(秒):',
|
duration: '壓測時長(秒)',
|
||||||
|
granularity: '聚合時間(秒)',
|
||||||
input_duration: '請輸入時長',
|
input_duration: '請輸入時長',
|
||||||
rps_limit: 'RPS上限:',
|
rps_limit: 'RPS上限:',
|
||||||
input_rps_limit: '請輸入限制',
|
input_rps_limit: '請輸入限制',
|
||||||
|
|
Loading…
Reference in New Issue