diff --git a/README.md b/README.md index 84c13e9b2..409b0e672 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Welcome to Tencent Cloud Software Development Kit (SDK), a companion tool for th Tencent Cloud SDK for Java helps Java developers debug and use TencentCloud APIs with ease. This document describes Tencent Cloud SDK for Java and how to quickly use it with code examples provided. # Dependent Environment -1. Dependent environment: JDK 7 or higher. +1. Dependent environment: JDK 8 or higher. 2. Activate your product in the Tencent Cloud Console. 3. Get the `SecretID`, `SecretKey`, and endpoint. The general format of endpoint is `\*.intl.tencentcloudapi.com`. For example, the endpoint of CVM is `cvm.intl.tencentcloudapi.com`. For more information, please see the documentation of the specified product. @@ -197,6 +197,29 @@ request.setHeader(header); ``` Example: [CustomHttpClient.java](examples/common/CustomHttpClient.java). +# OkHttp Upgrade + +To mitigate security risks in OkHttp 3.x, starting from version 3.2.0, the OkHttp dependency is upgraded from 3.12.13 to 4.12.0. + +### Roll back to OkHttp 3.12.13 + +If the upgrade conflicts with your project, you can explicitly declare the following dependencies in your application's `pom.xml` to roll back. Per Maven's "nearest definition" dependency mediation rule, the version declared by the application will override the 4.12.0 transitively introduced by the SDK: + +```xml + + com.squareup.okhttp3 + okhttp + 3.12.13 + + + com.squareup.okhttp3 + logging-interceptor + 3.12.13 + +``` + +> **Note**: Rolling back will reintroduce the security risks fixed in newer OkHttp versions. Use it only for emergency troubleshooting and issue diagnosis, and restore OkHttp 4 as soon as the issue is resolved. + # Other Issues ## Certificate Problems diff --git a/pom.xml b/pom.xml index 9b708e1b8..b2f8dc31d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ jar tencentcloud-sdk-java-intl-en https://cloud.tencent.com/ - Tencent Cloud API SDK for Java + Tencent Cloud Open API SDK for Java commons-logging @@ -17,7 +17,7 @@ com.squareup.okhttp3 okhttp - 3.12.13 + 4.12.0 com.google.code.gson @@ -27,12 +27,12 @@ com.squareup.okhttp3 logging-interceptor - 3.12.13 + 4.12.0 - org.ini4j - ini4j - 0.5.4 + org.apache.commons + commons-configuration2 + 2.12.0 junit @@ -40,11 +40,6 @@ 4.13.1 test - - com.tencentcloudapi - tencentcloud-sdk-java-common - 3.1.924 - UTF-8 @@ -68,8 +63,8 @@ maven-compiler-plugin 2.3.2 - 7 - 7 + 1.8 + 1.8 UTF-8 @@ -79,7 +74,7 @@ 2.3.2 - + @@ -154,3 +149,4 @@ + diff --git a/src/main/java/com/tencentcloudapi/common/AbstractClient.java b/src/main/java/com/tencentcloudapi/common/AbstractClient.java index 6f5392475..f7c9677a9 100644 --- a/src/main/java/com/tencentcloudapi/common/AbstractClient.java +++ b/src/main/java/com/tencentcloudapi/common/AbstractClient.java @@ -1,1104 +1,1232 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; -import com.tencentcloudapi.common.exception.TencentCloudSDKException; -import com.tencentcloudapi.common.http.HttpConnection; -import com.tencentcloudapi.common.profile.ClientProfile; -import com.tencentcloudapi.common.profile.HttpProfile; -import okhttp3.*; -import okhttp3.Headers.Builder; - -import javax.crypto.Mac; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.sql.Date; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * AbstractClient provides the basic functionalities for interacting with Tencent Cloud services. - * It handles request signing, sending, and response processing. - */ -public abstract class AbstractClient { - - public static final int HTTP_RSP_OK = 200; - public static final String SDK_VERSION = "SDK_JAVA_3.0.1403"; - public Gson gson; - - // User's security credentials (SecretId, SecretKey, Token). - private Credential credential; - - // Client configuration (e.g., timeout, endpoint). - private ClientProfile profile; - - // API endpoint URL. - private String endpoint; - - // Service name (e.g., "cvm"). - private String service; - - // Region (e.g., "ap-guangzhou"). - private String region; - - // API request path (usually "/"). - private String path; - - // SDK version string. - private String sdkVersion; - - // API version string. - private String apiVersion; - - // Logger for debugging and information. - private TCLog log; - - // Handles HTTP connections. - private HttpConnection httpConnection; - - // Circuit breaker for handling region failures. - private CircuitBreaker regionBreaker; - - /** - * Constructor for AbstractClient with default client profile. - * - * @param endpoint API endpoint URL. - * @param version API version. - * @param credential User credentials. - * @param region Region. - */ - public AbstractClient(String endpoint, String version, Credential credential, String region) { - this(endpoint, version, credential, region, new ClientProfile()); - } - - /** - * Constructor for AbstractClient with a custom client profile. - * - * @param endpoint API endpoint URL. - * @param version API version. - * @param credential User credentials. - * @param region Region. - * @param profile Client configuration profile. - */ - public AbstractClient( - String endpoint, - String version, - Credential credential, - String region, - ClientProfile profile) { - this.credential = credential; - this.profile = profile; - this.endpoint = endpoint; - this.service = endpoint.split("\\.")[0]; - this.region = region; - this.path = "/"; - this.sdkVersion = AbstractClient.SDK_VERSION; - this.apiVersion = version; - this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - this.log = new TCLog(getClass().getName(), profile.isDebug()); - this.httpConnection = new HttpConnection( - this.profile.getHttpProfile().getConnTimeout(), - this.profile.getHttpProfile().getReadTimeout(), - this.profile.getHttpProfile().getWriteTimeout() - ); - this.httpConnection.addInterceptors(this.log); - this.trySetProxy(this.httpConnection); - this.trySetSSLSocketFactory(this.httpConnection); - this.trySetRegionBreaker(); - this.trySetHostnameVerifier(this.httpConnection); - this.trySetHttpClient(); - warmup(); - } - - /** - * Gets the region. - * - * @return The region. - */ - public String getRegion() { - return this.region; - } - - /** - * Sets the region. - * - * @param region The region to set. - */ - public void setRegion(String region) { - this.region = region; - } - - /** - * Gets the client profile. - * - * @return The client profile. - */ - public ClientProfile getClientProfile() { - return this.profile; - } - - /** - * Sets the client profile. - * - * @param profile The client profile to set. - */ - public void setClientProfile(ClientProfile profile) { - this.profile = profile; - } - - /** - * Gets the credential. - * - * @return The credential. - */ - public Credential getCredential() { - return this.credential; - } - - /** - * Sets the credential. - * - * @param credential The credential to set. - */ - public void setCredential(Credential credential) { - this.credential = credential; - } - - /** - * Calls an API action with JSON payload using the TC3-HMAC-SHA256 signature. - * Ignores the request method and signature method defined in the profile. - * - * @param action Name of the API action. - * @param jsonPayload JSON string containing the request parameters. - * @return Raw response from the API. - * @throws TencentCloudSDKException If an error occurs during the API call. - */ - public String call(String action, String jsonPayload) throws TencentCloudSDKException { - Credential credSnapshot = this.credential.getSnapshot(); - HashMap headers = this.getHeaders(credSnapshot); - headers.put("X-TC-Action", action); - headers.put("Content-Type", "application/json; charset=utf-8"); - byte[] requestPayload = jsonPayload.getBytes(StandardCharsets.UTF_8); - String authorization = this.getAuthorization(headers, requestPayload, credSnapshot); - headers.put("Authorization", authorization); - String url = this.profile.getHttpProfile().getProtocol() + this.getEndpoint() + this.path; - return this.getResponseBody(url, headers, requestPayload); - } - - /** - * Calls an API action with binary payload using the TC3-HMAC-SHA256 signature. - * Ignores the request method and signature method defined in the profile. - * - * @param action Name of the API action. - * @param headers HTTP headers to include in the request. - * @param body Binary payload (octet-stream). - * @return Raw response from the API. - * @throws TencentCloudSDKException If an error occurs during the API call. - */ - public String callOctetStream(String action, HashMap headers, byte[] body) - throws TencentCloudSDKException { - Credential credSnapshot = this.credential.getSnapshot(); - headers.putAll(this.getHeaders(credSnapshot)); - headers.put("X-TC-Action", action); - headers.put("Content-Type", "application/octet-stream; charset=utf-8"); - String authorization = this.getAuthorization(headers, body, credSnapshot); - headers.put("Authorization", authorization); - String url = this.profile.getHttpProfile().getProtocol() + this.getEndpoint() + this.path; - return this.getResponseBody(url, headers, body); - } - - /** - * Generates common HTTP headers for Tencent Cloud API requests. - * - * @return A HashMap containing the headers. - */ - private HashMap getHeaders(Credential credSnapshot) { - HashMap headers = new HashMap(); - String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - headers.put("X-TC-Timestamp", timestamp); - headers.put("X-TC-Version", this.apiVersion); - headers.put("X-TC-Region", this.getRegion()); - headers.put("X-TC-RequestClient", SDK_VERSION); - headers.put("Host", this.getEndpoint()); - String token = credSnapshot.getToken(); - if (token != null && !token.isEmpty()) { - headers.put("X-TC-Token", token); - } - if (this.profile.isUnsignedPayload()) { - headers.put("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD"); - } - if (null != this.profile.getLanguage()) { - headers.put("X-TC-Language", this.profile.getLanguage().getValue()); - } - return headers; - } - - /** - * Generates the authorization header for TC3-HMAC-SHA256 signature. - * - * @param headers HTTP headers. - * @param body Request payload. - * @return The authorization header string. - * @throws TencentCloudSDKException If an error occurs during signature generation. - */ - private String getAuthorization(HashMap headers, byte[] body, Credential credSnapshot) - throws TencentCloudSDKException { - String endpoint = this.getEndpoint(); - // always use post tc3-hmac-sha256 signature process - // okhttp always set charset even we don't specify it, - // to ensure signature be correct, we have to set it here as well. - String contentType = headers.get("Content-Type"); - byte[] requestPayload = body; - String canonicalUri = "/"; - String canonicalQueryString = ""; - String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n"; - String signedHeaders = "content-type;host"; - - String hashedRequestPayload = ""; - if (this.profile.isUnsignedPayload()) { - hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8)); - } else { - hashedRequestPayload = Sign.sha256Hex(requestPayload); - } - String canonicalRequest = - HttpProfile.REQ_POST - + "\n" - + canonicalUri - + "\n" - + canonicalQueryString - + "\n" - + canonicalHeaders - + "\n" - + signedHeaders - + "\n" - + hashedRequestPayload; - - String timestamp = headers.get("X-TC-Timestamp"); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); - String service = endpoint.split("\\.")[0]; - String credentialScope = date + "/" + service + "/" + "tc3_request"; - String hashedCanonicalRequest = - Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); - String stringToSign = - "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; - - String secretId = credSnapshot.getSecretId(); - String secretKey = credSnapshot.getSecretKey(); - byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date); - byte[] secretService = Sign.hmac256(secretDate, service); - byte[] secretSigning = Sign.hmac256(secretService, "tc3_request"); - String signature = - DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase(); - return "TC3-HMAC-SHA256 " - + "Credential=" - + secretId - + "/" - + credentialScope - + ", " - + "SignedHeaders=" - + signedHeaders - + ", " - + "Signature=" - + signature; - } - - /** - * Sends the HTTP request and retrieves the response body. - * - * @param url The request URL. - * @param headers HTTP headers. - * @param body Request payload. - * @return The response body as a string. - * @throws TencentCloudSDKException If an error occurs during the request or response processing. - */ - private String getResponseBody(String url, HashMap headers, byte[] body) - throws TencentCloudSDKException { - Builder hb = new Builder(); - for (String key : headers.keySet()) { - hb.add(key, headers.get(key)); - } - Response resp = null; - try { - resp = this.httpConnection.postRequest(url, body, hb.build()); - } catch (IOException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e); - } - if (resp.code() != AbstractClient.HTTP_RSP_OK) { - String msg = "response code is " + resp.code() + ", not 200"; - log.info(msg); - throw new TencentCloudSDKException(msg, "", "ServerSideError"); - } - String respbody = null; - try { - respbody = resp.body().string(); - } catch (IOException e) { - String msg = - "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " + - "and stream length disagree."; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - JsonResponseModel errResp = null; - try { - Type errType = new TypeToken>() { - }.getType(); - errResp = gson.fromJson(respbody, errType); - } catch (JsonSyntaxException e) { - String msg = "json is not a valid representation for an object of type"; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - if (errResp.response.error != null) { - throw new TencentCloudSDKException( - errResp.response.error.message, errResp.response.requestId, errResp.response.error.code); - } - return respbody; - } - - private void trySetProxy(HttpConnection conn) { - String host = this.profile.getHttpProfile().getProxyHost(); - int port = this.profile.getHttpProfile().getProxyPort(); - - if (host == null || host.isEmpty()) { - return; - } - Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)); - conn.setProxy(proxy); - - final String username = this.profile.getHttpProfile().getProxyUsername(); - final String password = this.profile.getHttpProfile().getProxyPassword(); - if (username == null || username.isEmpty()) { - return; - } - conn.setProxyAuthenticator( - new Authenticator() { - @Override - public Request authenticate(Route route, Response response) throws IOException { - String credential = Credentials.basic(username, password); - return response - .request() - .newBuilder() - .header("Proxy-Authorization", credential) - .build(); - } - }); - } - - private void trySetSSLSocketFactory(HttpConnection conn) { - SSLSocketFactory sslSocketFactory = this.profile.getHttpProfile().getSslSocketFactory(); - X509TrustManager trustManager = this.profile.getHttpProfile().getX509TrustManager(); - if (sslSocketFactory != null) { - if (trustManager != null) { - this.httpConnection.setSSLSocketFactory(sslSocketFactory, trustManager); - } else { - this.httpConnection.setSSLSocketFactory(sslSocketFactory); - } - } - } - - private void trySetHostnameVerifier(HttpConnection conn) { - HostnameVerifier hostnameVerifier = this.profile.getHttpProfile().getHostnameVerifier(); - if (hostnameVerifier != null) { - this.httpConnection.setHostnameVerifier(hostnameVerifier); - } - } - - private void trySetRegionBreaker() { - String ep = profile.getBackupEndpoint(); - if (ep != null && !ep.isEmpty()) { - this.regionBreaker = new CircuitBreaker(); - } - } - - private void trySetHttpClient() { - Object httpClient = profile.getHttpProfile().getHttpClient(); - if (httpClient != null) { - this.httpConnection.setHttpClient(httpClient); - } - } - - /** - * Executes an API request and returns the raw string response. - * Handles circuit breaking for region failover. - * - * @param request The request object containing API parameters. - * @param actionName The name of the API action to be called. - * @return The raw string response from the API. - * @throws TencentCloudSDKException If an error occurs during the API call. - */ - protected String internalRequest(AbstractModel request, String actionName) - throws TencentCloudSDKException { - - CircuitBreaker.Token breakerToken = null; - // Attempt to acquire a token from the circuit breaker. - // If the circuit is open, use the backup endpoint. - if (regionBreaker != null) { - breakerToken = regionBreaker.allow(); - if (!breakerToken.allowed) { - endpoint = service + "." + profile.getBackupEndpoint(); - } - } - - Response okRsp; - try { - // Execute the raw API request. - okRsp = internalRequestRaw(request, actionName); - } catch (IOException e) { - // Network failure: report to circuit breaker and throw exception. - if (breakerToken != null) { - breakerToken.report(false); - } - throw new TencentCloudSDKException("", e); - } - - String strResp; - try { - // Extract the response body as a string. - strResp = okRsp.body().string(); - } catch (IOException e) { - String msg = "Cannot transfer response body to string, because Content-Length is too large, or " + - "Content-Length and stream length disagree."; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - - JsonResponseModel errResp; - try { - // Deserialize the response to check for errors. - Type errType = new TypeToken>() { - }.getType(); - errResp = gson.fromJson(strResp, errType); - } catch (JsonSyntaxException e) { - // Invalid JSON response: log and throw exception. - String msg = "json is not a valid representation for an object of type"; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - - // Check for API errors in the response. - if (errResp.response.error != null) { - if (breakerToken != null) { - // Report the success/failure of the request to the circuit breaker. - JsonResponseErrModel error = errResp.response; - // Consider a region "OK" if we get a valid requestId and no InternalError. - boolean regionOk = error.requestId != null - && !error.requestId.isEmpty() - && error.error.code != null - && !error.error.code.equals("InternalError"); - breakerToken.report(regionOk); - } - throw new TencentCloudSDKException( - errResp.response.error.message, - errResp.response.requestId, - errResp.response.error.code); - } - - return strResp; - } - - /** - * Executes an API request and returns the deserialized response object. - * Handles circuit breaking for region failover. - * - * @param request The request object containing API parameters. - * @param actionName The name of the API action to be called. - * @param typeOfT The class of the response object to deserialize to. - * @param The type of the response object. - * @return The deserialized response object. - * @throws TencentCloudSDKException If an error occurs during the API call. - */ - protected T internalRequest(AbstractModel request, String actionName, Class typeOfT) - throws TencentCloudSDKException { - CircuitBreaker.Token breakerToken = null; - // Attempt to acquire a token from the circuit breaker. - // If the circuit is open, use the backup endpoint. - if (regionBreaker != null) { - breakerToken = regionBreaker.allow(); - if (!breakerToken.allowed) { - endpoint = service + "." + profile.getBackupEndpoint(); - } - } - - try { - Response resp = internalRequestRaw(request, actionName); - if (Objects.equals(resp.header("Content-Type"), "text/event-stream")) { - return processResponseSSE(resp, typeOfT, breakerToken); - } - return processResponseJson(resp, typeOfT, breakerToken); - } catch (IOException e) { - // Network failure: report to circuit breaker and throw exception. - if (breakerToken != null) { - breakerToken.report(false); - } - throw new TencentCloudSDKException("", e); - } - } - - /** - * Processes a Server-Sent Events (SSE) response. - * - * @param resp The raw HTTP response. - * @param typeOfT The class of the response model. - * @param breakerToken The circuit breaker token. - * @param The type of the response model. - * @return The SSE response model. - * @throws TencentCloudSDKException If an error occurs during processing. - */ - protected T processResponseSSE(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) throws TencentCloudSDKException { - SSEResponseModel responseModel; - try { - // Create a new instance of the response model. - responseModel = (SSEResponseModel) typeOfT.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new TencentCloudSDKException("", e); - } - // Set request ID and circuit breaker token in the response model. - responseModel.setRequestId(resp.header("X-TC-RequestId")); - responseModel.setToken(breakerToken); - responseModel.setResponse(resp); - return (T) responseModel; - } - - /** - * Processes a JSON response. - * - * @param resp The raw HTTP response. - * @param typeOfT The class of the response object to deserialize to. - * @param breakerToken The circuit breaker token. - * @param The type of the response object. - * @return The deserialized response object. - * @throws TencentCloudSDKException If an error occurs during processing. - */ - protected T processResponseJson(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) throws TencentCloudSDKException { - String body; - try { - body = resp.body().string(); - } catch (IOException e) { - String msg = "Cannot transfer response body to string, because Content-Length is too large, or " + - "Content-Length and stream length disagree."; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - - JsonResponseModel errResp; - try { - Type errType = new TypeToken>() { - }.getType(); - errResp = gson.fromJson(body, errType); - } catch (JsonSyntaxException e) { - String msg = "json is not a valid representation for an object of type"; - log.info(msg); - throw new TencentCloudSDKException(msg, e); - } - - // Check for API errors in the response. - if (errResp.response.error != null) { - if (breakerToken != null) { - // Report the success/failure of the request to the circuit breaker. - JsonResponseErrModel error = errResp.response; - // Consider a region "OK" if we get a valid requestId and no InternalError. - boolean regionOk = error.requestId != null - && !error.requestId.isEmpty() - && error.error.code != null - && !error.error.code.equals("InternalError"); - breakerToken.report(regionOk); - } - throw new TencentCloudSDKException( - errResp.response.error.message, - errResp.response.requestId, - errResp.response.error.code); - } - - // Deserialize the successful response into the desired object type. - Type type = TypeToken.getParameterized(JsonResponseModel.class, typeOfT).getType(); - return ((JsonResponseModel) gson.fromJson(body, type)).response; - } - - /** - * Executes the raw API request and returns the HTTP Response object. - * - * @param request The request object containing API parameters. - * @param actionName The name of the API action to be called. - * @return The raw HTTP Response object. - * @throws TencentCloudSDKException If an error occurs during the API call. - * @throws IOException If an I/O error occurs. - */ - protected Response internalRequestRaw(AbstractModel request, String actionName) - throws TencentCloudSDKException, IOException { - Response okRsp = null; - String endpoint = this.getEndpoint(); - String[] binaryParams = request.getBinaryParams(); - String sm = this.profile.getSignMethod(); - String reqMethod = this.profile.getHttpProfile().getReqMethod(); - - // currently, customized params only can be supported via post json tc3-hmac-sha256 - HashMap customizedParams = request.any(); - if (customizedParams.size() > 0) { - if (binaryParams.length > 0) { - throw new TencentCloudSDKException( - "WrongUsage: Cannot post multipart with customized parameters."); - } - if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) { - throw new TencentCloudSDKException( - "WrongUsage: Cannot use HmacSHA1 or HmacSHA256 with customized parameters."); - } - if (reqMethod.equals(HttpProfile.REQ_GET)) { - throw new TencentCloudSDKException( - "WrongUsage: Cannot use get method with customized parameters."); - } - } - - - if (binaryParams.length > 0 || sm.equals(ClientProfile.SIGN_TC3_256)) { - okRsp = doRequestWithTC3(endpoint, request, actionName); - } else if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) { - okRsp = doRequest(endpoint, request, actionName); - } else { - throw new TencentCloudSDKException( - "Signature method " + sm + " is invalid or not supported yet."); - } - - // Check the HTTP response code. - if (okRsp.code() != AbstractClient.HTTP_RSP_OK) { - String msg = "response code is " + okRsp.code() + ", not 200"; - log.info(msg); - throw new TencentCloudSDKException(msg, "", "ServerSideError"); - } - return okRsp; - } - - /** - * Executes an API request using the older signature methods (HmacSHA1 or HmacSHA256). - * - * @param endpoint The API endpoint. - * @param request The request object. - * @param action The API action name. - * @return The HTTP Response object. - * @throws TencentCloudSDKException If an error occurs. - * @throws IOException If an I/O error occurs. - */ - private Response doRequest(String endpoint, AbstractModel request, String action) - throws TencentCloudSDKException, IOException { - HashMap param = new HashMap(); - request.toMap(param, ""); - String strParam = this.formatRequestData(action, param); - String reqMethod = this.profile.getHttpProfile().getReqMethod(); - String protocol = this.profile.getHttpProfile().getProtocol(); - String url = protocol + endpoint + this.path; - String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); - if (null != apigwEndpoint) { - url = protocol + apigwEndpoint; - } - if (reqMethod.equals(HttpProfile.REQ_GET)) { - return this.httpConnection.getRequest(url + "?" + strParam); - } else if (reqMethod.equals(HttpProfile.REQ_POST)) { - return this.httpConnection.postRequest(url, strParam); - } else { - throw new TencentCloudSDKException("Method only support (GET, POST)"); - } - } - - /** - * Executes an API request using the TC3-HMAC-SHA256 signature method. - * - * @param endpoint The API endpoint. - * @param request The request object. - * @param action The API action name. - * @return The HTTP Response object. - * @throws TencentCloudSDKException If an error occurs. - * @throws IOException If an I/O error occurs. - */ - private Response doRequestWithTC3(String endpoint, AbstractModel request, String action) - throws TencentCloudSDKException, IOException { - Credential credSnapshot = this.credential.getSnapshot(); - String httpRequestMethod = this.profile.getHttpProfile().getReqMethod(); - if (httpRequestMethod == null) { - throw new TencentCloudSDKException( - "Request method should not be null, can only be GET or POST"); - } - String contentType = "application/x-www-form-urlencoded"; - byte[] requestPayload = "".getBytes(StandardCharsets.UTF_8); - HashMap params = new HashMap(); - request.toMap(params, ""); - String[] binaryParams = request.getBinaryParams(); - if (binaryParams.length > 0) { - httpRequestMethod = HttpProfile.REQ_POST; - String boundary = UUID.randomUUID().toString(); - // okhttp always set charset even we don't specify it, - // to ensure signature be correct, we have to set it here as well. - contentType = "multipart/form-data; charset=utf-8" + "; boundary=" + boundary; - try { - requestPayload = getMultipartPayload(request, boundary); - } catch (Exception e) { - throw new TencentCloudSDKException("Failed to generate multipart.", e); - } - } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) { - requestPayload = AbstractModel.toJsonString(request).getBytes(StandardCharsets.UTF_8); - // okhttp always set charset even we don't specify it, - // to ensure signature be correct, we have to set it here as well. - contentType = "application/json; charset=utf-8"; - } - // Construct the canonical request for signature calculation. - String canonicalUri = "/"; - String canonicalQueryString = this.getCanonicalQueryString(params, httpRequestMethod); - String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n"; - String signedHeaders = "content-type;host"; - - String hashedRequestPayload = ""; - if (this.profile.isUnsignedPayload()) { - hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8)); - } else { - hashedRequestPayload = Sign.sha256Hex(requestPayload); - } - String canonicalRequest = - httpRequestMethod - + "\n" - + canonicalUri - + "\n" - + canonicalQueryString - + "\n" - + canonicalHeaders - + "\n" - + signedHeaders - + "\n" - + hashedRequestPayload; - - String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - sdf.setTimeZone(TimeZone.getTimeZone("UTC")); - String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); - String service = endpoint.split("\\.")[0]; - String credentialScope = date + "/" + service + "/" + "tc3_request"; - String hashedCanonicalRequest = - Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); - String stringToSign = - "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; - boolean skipSign = request.getSkipSign(); - String authorization = ""; - if (skipSign) { - authorization = "SKIP"; - } else { - String secretId = credSnapshot.getSecretId(); - String secretKey = credSnapshot.getSecretKey(); - byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date); - byte[] secretService = Sign.hmac256(secretDate, service); - byte[] secretSigning = Sign.hmac256(secretService, "tc3_request"); - String signature = - DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase(); - authorization = - "TC3-HMAC-SHA256 " - + "Credential=" - + secretId - + "/" - + credentialScope - + ", " - + "SignedHeaders=" - + signedHeaders - + ", " - + "Signature=" - + signature; - } - Builder hb = new Builder(); - hb.add("Content-Type", contentType) - .add("Host", endpoint) - .add("Authorization", authorization) - .add("X-TC-Action", action) - .add("X-TC-Timestamp", timestamp) - .add("X-TC-Version", this.apiVersion) - .add("X-TC-RequestClient", SDK_VERSION); - if (null != request.GetHeader()) { - for (Map.Entry entry : request.GetHeader().entrySet()) { - hb.add(entry.getKey(), entry.getValue()); - } - } - if (null != this.getRegion()) { - hb.add("X-TC-Region", this.getRegion()); - } - String token = credSnapshot.getToken(); - if (token != null && !token.isEmpty()) { - hb.add("X-TC-Token", token); - } - if (this.profile.isUnsignedPayload()) { - hb.add("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD"); - } - if (null != this.profile.getLanguage()) { - hb.add("X-TC-Language", this.profile.getLanguage().getValue()); - } - - String protocol = this.profile.getHttpProfile().getProtocol(); - String url = protocol + endpoint + this.path; - String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); - if (null != apigwEndpoint) { - url = protocol + apigwEndpoint; - hb.set("Host", apigwEndpoint); - } - Headers headers = hb.build(); - if (httpRequestMethod.equals(HttpProfile.REQ_GET)) { - return this.httpConnection.getRequest(url + "?" + canonicalQueryString, headers); - } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) { - return this.httpConnection.postRequest(url, requestPayload, headers); - } else { - throw new TencentCloudSDKException("Method only support GET, POST"); - } - } - - /** - * Constructs the multipart payload for file uploads. - * - * @param request The request object containing file parameters. - * @param boundary The boundary string to separate parts of the multipart data. - * @return The byte array representing the multipart payload. - * @throws Exception If an error occurs during payload construction. - */ - private byte[] getMultipartPayload(AbstractModel request, String boundary) throws Exception { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - String[] binaryParams = request.getBinaryParams(); - // Iterate through each parameter in the multipart request. - for (Map.Entry entry : request.getMultipartRequestParams().entrySet()) { - baos.write("--".getBytes(StandardCharsets.UTF_8)); - baos.write(boundary.getBytes(StandardCharsets.UTF_8)); - baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); - baos.write("Content-Disposition: form-data; name=\"".getBytes(StandardCharsets.UTF_8)); - baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8)); - if (Arrays.asList(binaryParams).contains(entry.getKey())) { - baos.write("\"; filename=\"".getBytes(StandardCharsets.UTF_8)); - baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8)); - baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8)); - } else { - baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8)); - } - baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); - baos.write(entry.getValue()); - baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); - } - // Write the closing boundary if there's any data. - if (baos.size() != 0) { - baos.write("--".getBytes(StandardCharsets.UTF_8)); - baos.write(boundary.getBytes(StandardCharsets.UTF_8)); - baos.write("--\r\n".getBytes(StandardCharsets.UTF_8)); - } - byte[] bytes = baos.toByteArray(); - baos.close(); - return bytes; - } - - /** - * Generates the canonical query string for GET requests. - * - * @param params The map of request parameters. - * @param method The HTTP method (should be GET). - * @return The canonical query string. - * @throws TencentCloudSDKException If UTF-8 encoding is not supported. - */ - private String getCanonicalQueryString(HashMap params, String method) - throws TencentCloudSDKException { - // POST requests don't have a query string in the signature. - if (method != null && method.equals(HttpProfile.REQ_POST)) { - return ""; - } - StringBuilder queryString = new StringBuilder(""); - // Iterate through each parameter and build the query string. - for (Map.Entry entry : params.entrySet()) { - String v; - try { - v = URLEncoder.encode(entry.getValue(), "UTF8"); - } catch (UnsupportedEncodingException e) { - throw new TencentCloudSDKException("UTF8 is not supported.", e); - } - queryString.append("&").append(entry.getKey()).append("=").append(v); - } - // Remove the leading '&' if the query string is not empty. - if (queryString.length() == 0) { - return ""; - } else { - return queryString.toString().substring(1); - } - } - - /** - * Formats the request data for signing (older signature methods). - * - * @param action The API action name. - * @param param The map of request parameters. - * @return The formatted string for signing. - * @throws TencentCloudSDKException If UTF-8 encoding is not supported. - */ - private String formatRequestData(String action, Map param) - throws TencentCloudSDKException { - Credential credSnapshot = this.credential.getSnapshot(); - String secretId = credSnapshot.getSecretId(); - String secretKey = credSnapshot.getSecretKey(); - String token = credSnapshot.getToken(); - param.put("Action", action); - param.put("RequestClient", this.sdkVersion); - param.put("Nonce", String.valueOf(Math.abs(new SecureRandom().nextInt()))); - param.put("Timestamp", String.valueOf(System.currentTimeMillis() / 1000)); - param.put("Version", this.apiVersion); - - // Add SecretId, Region, SignatureMethod, and Token if available. - if (secretId != null && (!secretId.isEmpty())) { - param.put("SecretId", secretId); - } - - if (this.region != null && (!this.region.isEmpty())) { - param.put("Region", this.region); - } - - if (this.profile.getSignMethod() != null && (!this.profile.getSignMethod().isEmpty())) { - param.put("SignatureMethod", this.profile.getSignMethod()); - } - - if (token != null && (!token.isEmpty())) { - param.put("Token", token); - } - - if (null != this.profile.getLanguage()) { - param.put("Language", this.profile.getLanguage().getValue()); - } - - String endpoint = this.getEndpoint(); - - // Generate the string to be signed. - String sigInParam = - Sign.makeSignPlainText( - new TreeMap(param), - this.profile.getHttpProfile().getReqMethod(), - endpoint, - this.path); - // Generate the signature. - String sigOutParam = - Sign.sign(secretKey, sigInParam, this.profile.getSignMethod()); - - String strParam = ""; - try { - // URL-encode each parameter and construct the query string. - for (Map.Entry entry : param.entrySet()) { - strParam += - (URLEncoder.encode(entry.getKey(), "utf-8") - + "=" - + URLEncoder.encode(entry.getValue(), "utf-8") - + "&"); - } - strParam += ("Signature=" + URLEncoder.encode(sigOutParam, "utf-8")); - } catch (UnsupportedEncodingException e) { - throw new TencentCloudSDKException("", e); - } - return strParam; - } - - /** - * Performs initializations to avoid performance costs in the first request. - */ - private void warmup() { - try { - // Initialize Mac instances (used for signature calculation). - // First invoke costs around 250 ms. - Mac.getInstance("HmacSHA1"); - Mac.getInstance("HmacSHA256"); - // Initialize SSLContext (used for HTTPS connections). - // First invoke costs around 150 ms. - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - } catch (Exception e) { - // Ignore but print the stack trace to the console for debugging. - e.printStackTrace(); - } - } - - /** - * Gets the API endpoint. - * - * @return The API endpoint URL. - */ - private String getEndpoint() { - // Use the endpoint from the profile if it's set, otherwise construct it from service and domain. - if (null != this.profile.getHttpProfile().getEndpoint()) { - return this.profile.getHttpProfile().getEndpoint(); - } else { - // protected abstract String getService(); - // use this.getService() from overrided subclass will be better - return this.service + "." + this.profile.getHttpProfile().getRootDomain(); - } - } - - /** - * 请注意购买类接口谨慎调用,可能导致多次购买 - * 仅幂等接口推荐使用 - * - * @param req - * @param retryTimes - * @throws TencentCloudSDKException - */ - public Object retry(AbstractModel req, int retryTimes) throws TencentCloudSDKException { - if (retryTimes < 0 || retryTimes > 10) { - throw new TencentCloudSDKException("The number of retryTimes supported is 0 to 10.", "", "ClientSideError"); - } - Class cls = this.getClass(); - String methodName = req.getClass().getSimpleName().replace("Request", ""); - Method method; - try { - method = cls.getMethod(methodName, req.getClass()); - } catch (NoSuchMethodException e) { - throw new TencentCloudSDKException("ClientSideError", e); - } - do { - try { - return method.invoke(this, req); - } catch (IllegalAccessException e) { - throw new TencentCloudSDKException("ClientSideError", e); - } catch (InvocationTargetException e) { - if (retryTimes == 0) { - throw (TencentCloudSDKException) e.getTargetException(); - } - } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new TencentCloudSDKException("ClientSideError", e); - } - } while (--retryTimes >= 0); - return null; - } - - public CircuitBreaker getRegionBreaker() { - return regionBreaker; - } - - public void setRegionBreaker(CircuitBreaker regionBreaker) { - this.regionBreaker = regionBreaker; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.http.HttpConnection; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import okhttp3.*; +import okhttp3.Headers.Builder; + +import javax.crypto.Mac; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * AbstractClient provides the basic functionalities for interacting with Tencent Cloud services. + * It handles request signing, sending, and response processing. + */ +public abstract class AbstractClient { + + public static final int HTTP_RSP_OK = 200; + public static final String SDK_VERSION = "SDK_JAVA_3.0.1403"; + public Gson gson; + + // User's security credentials (SecretId, SecretKey, Token). + private Credential credential; + + // Client configuration (e.g., timeout, endpoint). + private ClientProfile profile; + + // API endpoint URL. + private String endpoint; + + // Service name (e.g., "cvm"). + private String service; + + // Region (e.g., "ap-guangzhou"). + private String region; + + // API request path (usually "/"). + private String path; + + // SDK version string. + private String sdkVersion; + + // API version string. + private String apiVersion; + + // Logger for debugging and information. + private TCLog log; + + // Handles HTTP connections. + private HttpConnection httpConnection; + + /** + * Constructor for AbstractClient with default client profile. + * + * @param endpoint API endpoint URL. + * @param version API version. + * @param credential User credentials. + * @param region Region. + */ + public AbstractClient(String endpoint, String version, Credential credential, String region) { + this(endpoint, version, credential, region, new ClientProfile()); + } + + /** + * Constructor for AbstractClient with a custom client profile. + * + * @param endpoint API endpoint URL. + * @param version API version. + * @param credential User credentials. + * @param region Region. + * @param profile Client configuration profile. + */ + public AbstractClient( + String endpoint, + String version, + Credential credential, + String region, + ClientProfile profile) { + this.credential = credential; + this.profile = profile; + this.endpoint = endpoint; + this.service = endpoint.split("\\.")[0]; + this.region = region; + this.path = "/"; + this.sdkVersion = AbstractClient.SDK_VERSION; + this.apiVersion = version; + this.gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + this.log = new TCLog(getClass().getName(), profile.isDebug()); + this.httpConnection = new HttpConnection( + this.profile.getHttpProfile().getConnTimeout(), + this.profile.getHttpProfile().getReadTimeout(), + this.profile.getHttpProfile().getWriteTimeout() + ); + this.httpConnection.addInterceptors(this.log); + this.trySetProxy(this.httpConnection); + this.trySetSSLSocketFactory(this.httpConnection); + if (this.profile.isEnableDomainFailover()) { + this.httpConnection.addInterceptors(new EndpointFailoverInterceptor(this)); + } + this.trySetHostnameVerifier(this.httpConnection); + this.trySetHttpClient(); + warmup(); + } + + /** + * Gets the region. + * + * @return The region. + */ + public String getRegion() { + return this.region; + } + + /** + * Sets the region. + * + * @param region The region to set. + */ + public void setRegion(String region) { + this.region = region; + } + + /** + * Gets the client profile. + * + * @return The client profile. + */ + public ClientProfile getClientProfile() { + return this.profile; + } + + /** + * Sets the client profile. + * + * @param profile The client profile to set. + */ + public void setClientProfile(ClientProfile profile) { + this.profile = profile; + } + + /** + * Gets the credential. + * + * @return The credential. + */ + public Credential getCredential() { + return this.credential; + } + + /** + * Sets the credential. + * + * @param credential The credential to set. + */ + public void setCredential(Credential credential) { + this.credential = credential; + } + + /** + * Calls an API action with JSON payload using the TC3-HMAC-SHA256 signature. + * Ignores the request method and signature method defined in the profile. + * + * @param action Name of the API action. + * @param jsonPayload JSON string containing the request parameters. + * @return Raw response from the API. + * @throws TencentCloudSDKException If an error occurs during the API call. + */ + public String call(String action, String jsonPayload) throws TencentCloudSDKException { + Credential credSnapshot = this.credential.getSnapshot(); + byte[] requestPayload = jsonPayload.getBytes(StandardCharsets.UTF_8); + String endpoint = this.getEndpoint(); + String protocol = this.profile.getHttpProfile().getProtocol(); + String url = protocol + endpoint + this.path; + String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); + if (null != apigwEndpoint) { + url = protocol + apigwEndpoint; + } + Headers.Builder callerHeaders = new Headers.Builder(); + RequestBuilder rb = RequestBuilder.create() + .fromClient(this) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withURL(HttpUrl.parse(url)) + .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint) + .withMethod(HttpProfile.REQ_POST) + .withContentTypeJson() + .withPayload(requestPayload) + .withHeaders(callerHeaders.build()) + .withAction(action) + .withVersion(this.apiVersion) + .withRegion(this.region) + .withRequestClient(SDK_VERSION); + Response resp; + try { + resp = this.httpConnection.doRequest(rb.build()); + } catch (IOException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e); + } + return parseResponseBody(resp); + } + + /** + * Calls an API action with binary payload using the TC3-HMAC-SHA256 signature. + * Ignores the request method and signature method defined in the profile. + * + * @param action Name of the API action. + * @param headers HTTP headers to include in the request. + * @param body Binary payload (octet-stream). + * @return Raw response from the API. + * @throws TencentCloudSDKException If an error occurs during the API call. + */ + public String callOctetStream(String action, HashMap headers, byte[] body) + throws TencentCloudSDKException { + String endpoint = this.getEndpoint(); + String protocol = this.profile.getHttpProfile().getProtocol(); + String url = protocol + endpoint + this.path; + String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); + if (null != apigwEndpoint) { + url = protocol + apigwEndpoint; + } + Headers.Builder callerHeaders = new Headers.Builder(); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + callerHeaders.add(entry.getKey(), entry.getValue()); + } + } + RequestBuilder rb = RequestBuilder.create() + .fromClient(this) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withURL(HttpUrl.parse(url)) + .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint) + .withMethod(HttpProfile.REQ_POST) + .withContentTypeOctetStream() + .withPayload(body) + .withHeaders(callerHeaders.build()) + .withAction(action) + .withVersion(this.apiVersion) + .withRegion(this.region) + .withRequestClient(SDK_VERSION); + Response resp; + try { + resp = this.httpConnection.doRequest(rb.build()); + } catch (IOException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e); + } + return parseResponseBody(resp); + } + + /** + * Generates common HTTP headers for Tencent Cloud API requests. + * + * @param credSnapshot Atomic snapshot of the credential to read from. + * @return A HashMap containing the headers. + */ + private HashMap getHeaders(Credential credSnapshot) { + HashMap headers = new HashMap(); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + headers.put("X-TC-Timestamp", timestamp); + headers.put("X-TC-Version", this.apiVersion); + headers.put("X-TC-Region", this.getRegion()); + headers.put("X-TC-RequestClient", SDK_VERSION); + headers.put("Host", this.getEndpoint()); + String token = credSnapshot.getToken(); + if (token != null && !token.isEmpty()) { + headers.put("X-TC-Token", token); + } + if (this.profile.isUnsignedPayload()) { + headers.put("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD"); + } + if (null != this.profile.getLanguage()) { + headers.put("X-TC-Language", this.profile.getLanguage().getValue()); + } + return headers; + } + + /** + * Generates the authorization header for TC3-HMAC-SHA256 signature. + * + * @param headers HTTP headers. + * @param body Request payload. + * @return The authorization header string. + * @throws TencentCloudSDKException If an error occurs during signature generation. + */ + private String getAuthorization(HashMap headers, byte[] body, Credential credSnapshot) + throws TencentCloudSDKException { + String endpoint = this.getEndpoint(); + // always use post tc3-hmac-sha256 signature process + // okhttp always set charset even we don't specify it, + // to ensure signature be correct, we have to set it here as well. + String contentType = headers.get("Content-Type"); + byte[] requestPayload = body; + String canonicalUri = "/"; + String canonicalQueryString = ""; + String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n"; + String signedHeaders = "content-type;host"; + + String hashedRequestPayload = ""; + if (this.profile.isUnsignedPayload()) { + hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8)); + } else { + hashedRequestPayload = Sign.sha256Hex(requestPayload); + } + String canonicalRequest = + HttpProfile.REQ_POST + + "\n" + + canonicalUri + + "\n" + + canonicalQueryString + + "\n" + + canonicalHeaders + + "\n" + + signedHeaders + + "\n" + + hashedRequestPayload; + + String timestamp = headers.get("X-TC-Timestamp"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); + String service = endpoint.split("\\.")[0]; + String credentialScope = date + "/" + service + "/" + "tc3_request"; + String hashedCanonicalRequest = + Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); + String stringToSign = + "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; + + String secretId = credSnapshot.getSecretId(); + String secretKey = credSnapshot.getSecretKey(); + byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date); + byte[] secretService = Sign.hmac256(secretDate, service); + byte[] secretSigning = Sign.hmac256(secretService, "tc3_request"); + String signature = + DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase(); + return "TC3-HMAC-SHA256 " + + "Credential=" + + secretId + + "/" + + credentialScope + + ", " + + "SignedHeaders=" + + signedHeaders + + ", " + + "Signature=" + + signature; + } + + /** + * Sends the HTTP request and retrieves the response body. + * + * @param url The request URL. + * @param headers HTTP headers. + * @param body Request payload. + * @return The response body as a string. + * @throws TencentCloudSDKException If an error occurs during the request or response processing. + */ + private String getResponseBody(String url, HashMap headers, byte[] body) + throws TencentCloudSDKException { + Builder hb = new Headers.Builder(); + for (String key : headers.keySet()) { + hb.add(key, headers.get(key)); + } + Response resp = null; + try { + resp = this.httpConnection.postRequest(url, body, hb.build()); + } catch (IOException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage(), e); + } + if (resp.code() != AbstractClient.HTTP_RSP_OK) { + String msg = "response code is " + resp.code() + ", not 200"; + log.info(msg); + throw new TencentCloudSDKException(msg, "", "ServerSideError"); + } + String respbody = null; + try { + respbody = resp.body().string(); + } catch (IOException e) { + String msg = + "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " + + "and stream length disagree."; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + JsonResponseModel errResp = null; + try { + Type errType = new TypeToken>() { + }.getType(); + errResp = gson.fromJson(respbody, errType); + } catch (JsonSyntaxException e) { + String msg = "json is not a valid representation for an object of type"; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + if (errResp.response.error != null) { + throw new TencentCloudSDKException( + errResp.response.error.message, errResp.response.requestId, errResp.response.error.code); + } + return respbody; + } + + private String parseResponseBody(Response resp) throws TencentCloudSDKException { + if (resp.code() != AbstractClient.HTTP_RSP_OK) { + String msg = "response code is " + resp.code() + ", not 200"; + log.info(msg); + throw new TencentCloudSDKException(msg, "", "ServerSideError"); + } + String respbody = null; + try { + respbody = resp.body().string(); + } catch (IOException e) { + String msg = + "Cannot transfer response body to string, because Content-Length is too large, or Content-Length " + + "and stream length disagree."; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + JsonResponseModel errResp = null; + try { + Type errType = new TypeToken>() { + }.getType(); + errResp = gson.fromJson(respbody, errType); + } catch (JsonSyntaxException e) { + String msg = "json is not a valid representation for an object of type"; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + if (errResp.response.error != null) { + throw new TencentCloudSDKException( + errResp.response.error.message, errResp.response.requestId, errResp.response.error.code); + } + return respbody; + } + + private void trySetProxy(HttpConnection conn) { + String host = this.profile.getHttpProfile().getProxyHost(); + int port = this.profile.getHttpProfile().getProxyPort(); + + if (host == null || host.isEmpty()) { + return; + } + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)); + conn.setProxy(proxy); + + final String username = this.profile.getHttpProfile().getProxyUsername(); + String configuredPassword = this.profile.getHttpProfile().getProxyPassword(); + final String password = configuredPassword == null ? "" : configuredPassword; + if (username == null || username.isEmpty()) { + return; + } + conn.setProxyAuthenticator( + new Authenticator() { + @Override + public Request authenticate(Route route, Response response) throws IOException { + String credential = Credentials.basic(username, password); + return response + .request() + .newBuilder() + .header("Proxy-Authorization", credential) + .build(); + } + }); + } + + private void trySetSSLSocketFactory(HttpConnection conn) { + SSLSocketFactory sslSocketFactory = this.profile.getHttpProfile().getSslSocketFactory(); + X509TrustManager trustManager = this.profile.getHttpProfile().getX509TrustManager(); + if (sslSocketFactory != null) { + if (trustManager != null) { + this.httpConnection.setSSLSocketFactory(sslSocketFactory, trustManager); + } else { + this.httpConnection.setSSLSocketFactory(sslSocketFactory); + } + } + } + + private void trySetHostnameVerifier(HttpConnection conn) { + HostnameVerifier hostnameVerifier = this.profile.getHttpProfile().getHostnameVerifier(); + if (hostnameVerifier != null) { + this.httpConnection.setHostnameVerifier(hostnameVerifier); + } + } + + private void trySetHttpClient() { + Object httpClient = profile.getHttpProfile().getHttpClient(); + if (httpClient != null) { + this.httpConnection.setHttpClient(httpClient); + } + } + + /** + * Executes an API request and returns the raw string response. + * + * @param request The request object containing API parameters. + * @param actionName The name of the API action to be called. + * @return The raw string response from the API. + * @throws TencentCloudSDKException If an error occurs during the API call. + */ + protected String internalRequest(AbstractModel request, String actionName) + throws TencentCloudSDKException { + + Response okRsp; + try { + okRsp = internalRequestRaw(request, actionName); + } catch (IOException e) { + throw new TencentCloudSDKException("", e); + } + + String strResp; + try { + strResp = okRsp.body().string(); + } catch (IOException e) { + String msg = "Cannot transfer response body to string, because Content-Length is too large, or " + + "Content-Length and stream length disagree."; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + + JsonResponseModel errResp; + try { + Type errType = new TypeToken>() { + }.getType(); + errResp = gson.fromJson(strResp, errType); + } catch (JsonSyntaxException e) { + String msg = "json is not a valid representation for an object of type"; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + + if (errResp.response.error != null) { + throw new TencentCloudSDKException( + errResp.response.error.message, + errResp.response.requestId, + errResp.response.error.code); + } + + return strResp; + } + + /** + * Executes an API request and returns the deserialized response object. + * + * @param request The request object containing API parameters. + * @param actionName The name of the API action to be called. + * @param typeOfT The class of the response object to deserialize to. + * @param The type of the response object. + * @return The deserialized response object. + * @throws TencentCloudSDKException If an error occurs during the API call. + */ + protected T internalRequest(AbstractModel request, String actionName, Class typeOfT) + throws TencentCloudSDKException { + try { + Response resp = internalRequestRaw(request, actionName); + if (Objects.equals(resp.header("Content-Type"), "text/event-stream")) { + return processResponseSSE(resp, typeOfT); + } + return processResponseJson(resp, typeOfT); + } catch (IOException e) { + throw new TencentCloudSDKException("", e); + } + } + + /** + * Processes a Server-Sent Events (SSE) response. + * + * @param resp The raw HTTP response. + * @param typeOfT The class of the response model. + * @param The type of the response model. + * @return The SSE response model. + * @throws TencentCloudSDKException If an error occurs during processing. + */ + protected T processResponseSSE(Response resp, Class typeOfT) throws TencentCloudSDKException { + SSEResponseModel responseModel; + try { + responseModel = (SSEResponseModel) typeOfT.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new TencentCloudSDKException("", e); + } + responseModel.setRequestId(resp.header("X-TC-RequestId")); + responseModel.setResponse(resp); + return (T) responseModel; + } + + /** + * Legacy three-arg overload. The {@code breakerToken} is ignored — region + * failover is now handled by {@link EndpointFailoverInterceptor} at the HTTP + * layer, not via a per-call CircuitBreaker token. Kept so subclasses or + * external callers compiled against earlier SDK versions still link. + * + * @deprecated Use {@link #processResponseSSE(Response, Class)} instead. + */ + @Deprecated + protected T processResponseSSE(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) + throws TencentCloudSDKException { + return processResponseSSE(resp, typeOfT); + } + + /** + * Processes a JSON response. + * + * @param resp The raw HTTP response. + * @param typeOfT The class of the response object to deserialize to. + * @param The type of the response object. + * @return The deserialized response object. + * @throws TencentCloudSDKException If an error occurs during processing. + */ + protected T processResponseJson(Response resp, Class typeOfT) throws TencentCloudSDKException { + String body; + try { + body = resp.body().string(); + } catch (IOException e) { + String msg = "Cannot transfer response body to string, because Content-Length is too large, or " + + "Content-Length and stream length disagree."; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + + JsonResponseModel errResp; + try { + Type errType = new TypeToken>() { + }.getType(); + errResp = gson.fromJson(body, errType); + } catch (JsonSyntaxException e) { + String msg = "json is not a valid representation for an object of type"; + log.info(msg); + throw new TencentCloudSDKException(msg, e); + } + + if (errResp.response.error != null) { + throw new TencentCloudSDKException( + errResp.response.error.message, + errResp.response.requestId, + errResp.response.error.code); + } + + Type type = TypeToken.getParameterized(JsonResponseModel.class, typeOfT).getType(); + return ((JsonResponseModel) gson.fromJson(body, type)).response; + } + + /** + * Legacy three-arg overload. The {@code breakerToken} is ignored — region + * failover is now handled by {@link EndpointFailoverInterceptor} at the HTTP + * layer, not via a per-call CircuitBreaker token. Kept so subclasses or + * external callers compiled against earlier SDK versions still link. + * + * @deprecated Use {@link #processResponseJson(Response, Class)} instead. + */ + @Deprecated + protected T processResponseJson(Response resp, Class typeOfT, CircuitBreaker.Token breakerToken) + throws TencentCloudSDKException { + return processResponseJson(resp, typeOfT); + } + + /** + * Executes the raw API request and returns the HTTP Response object. + * + * @param request The request object containing API parameters. + * @param actionName The name of the API action to be called. + * @return The raw HTTP Response object. + * @throws TencentCloudSDKException If an error occurs during the API call. + * @throws IOException If an I/O error occurs. + */ + protected Response internalRequestRaw(AbstractModel request, String actionName) + throws TencentCloudSDKException, IOException { + Response okRsp = null; + String endpoint = this.getEndpoint(); + String[] binaryParams = request.getBinaryParams(); + String sm = this.profile.getSignMethod(); + String reqMethod = this.profile.getHttpProfile().getReqMethod(); + + // currently, customized params only can be supported via post json tc3-hmac-sha256 + HashMap customizedParams = request.any(); + if (customizedParams.size() > 0) { + if (binaryParams.length > 0) { + throw new TencentCloudSDKException( + "WrongUsage: Cannot post multipart with customized parameters."); + } + if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) { + throw new TencentCloudSDKException( + "WrongUsage: Cannot use HmacSHA1 or HmacSHA256 with customized parameters."); + } + if (reqMethod.equals(HttpProfile.REQ_GET)) { + throw new TencentCloudSDKException( + "WrongUsage: Cannot use get method with customized parameters."); + } + } + + + if (binaryParams.length > 0 || sm.equals(ClientProfile.SIGN_TC3_256)) { + okRsp = doRequestWithTC3(endpoint, request, actionName); + } else if (sm.equals(ClientProfile.SIGN_SHA1) || sm.equals(ClientProfile.SIGN_SHA256)) { + okRsp = doRequest(endpoint, request, actionName); + } else { + throw new TencentCloudSDKException( + "Signature method " + sm + " is invalid or not supported yet."); + } + + // Check the HTTP response code. + if (okRsp.code() != AbstractClient.HTTP_RSP_OK) { + String msg = "response code is " + okRsp.code() + ", not 200"; + log.info(msg); + throw new TencentCloudSDKException(msg, "", "ServerSideError"); + } + return okRsp; + } + + /** + * Executes an API request using the older signature methods (HmacSHA1 or HmacSHA256). + * + * @param endpoint The API endpoint. + * @param request The request object. + * @param action The API action name. + * @return The HTTP Response object. + * @throws TencentCloudSDKException If an error occurs. + * @throws IOException If an I/O error occurs. + */ + private Response doRequest(String endpoint, AbstractModel request, String action) + throws TencentCloudSDKException, IOException { + String reqMethod = this.profile.getHttpProfile().getReqMethod(); + String protocol = this.profile.getHttpProfile().getProtocol(); + String url = protocol + endpoint + this.path; + String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); + if (null != apigwEndpoint) { + url = protocol + apigwEndpoint; + } + Headers.Builder callerHeaders = new Headers.Builder(); + if (null != request.GetHeader()) { + for (Map.Entry entry : request.GetHeader().entrySet()) { + callerHeaders.add(entry.getKey(), entry.getValue()); + } + } + RequestBuilder rb = RequestBuilder.create() + .fromClient(this) + .withSignMethod(this.profile.getSignMethod()) + .withURL(HttpUrl.parse(url)) + .withHost(endpoint) + .withMethod(reqMethod) + .withContentTypeForm() + .withPayload(request) + .withHeaders(callerHeaders.build()) + .withAction(action) + .withVersion(this.apiVersion) + .withRegion(this.region) + .withRequestClient(SDK_VERSION) + .withNonce(Math.abs(new SecureRandom().nextInt())); + return this.httpConnection.doRequest(rb.build()); + } + + /** + * Executes an API request using the TC3-HMAC-SHA256 signature method. + * + * @param endpoint The API endpoint. + * @param request The request object. + * @param action The API action name. + * @return The HTTP Response object. + * @throws TencentCloudSDKException If an error occurs. + * @throws IOException If an I/O error occurs. + */ + private Response doRequestWithTC3(String endpoint, AbstractModel request, String action) + throws TencentCloudSDKException, IOException { + Credential credSnapshot = this.credential.getSnapshot(); + String httpRequestMethod = this.profile.getHttpProfile().getReqMethod(); + if (httpRequestMethod == null) { + throw new TencentCloudSDKException( + "Request method should not be null, can only be GET or POST"); + } + String contentType = "application/x-www-form-urlencoded"; + byte[] requestPayload = "".getBytes(StandardCharsets.UTF_8); + HashMap params = new HashMap(); + request.toMap(params, ""); + String[] binaryParams = request.getBinaryParams(); + if (binaryParams.length > 0) { + httpRequestMethod = HttpProfile.REQ_POST; + String boundary = UUID.randomUUID().toString(); + // okhttp always set charset even we don't specify it, + // to ensure signature be correct, we have to set it here as well. + contentType = "multipart/form-data; charset=utf-8" + "; boundary=" + boundary; + try { + requestPayload = getMultipartPayload(request, boundary); + } catch (Exception e) { + throw new TencentCloudSDKException("Failed to generate multipart.", e); + } + } else if (httpRequestMethod.equals(HttpProfile.REQ_POST)) { + requestPayload = AbstractModel.toJsonString(request).getBytes(StandardCharsets.UTF_8); + // okhttp always set charset even we don't specify it, + // to ensure signature be correct, we have to set it here as well. + contentType = "application/json; charset=utf-8"; + } + if (binaryParams.length == 0) { + String protocol = this.profile.getHttpProfile().getProtocol(); + String url = protocol + endpoint + this.path; + String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); + if (null != apigwEndpoint) { + url = protocol + apigwEndpoint; + } + Headers.Builder callerHeaders = new Headers.Builder(); + if (null != request.GetHeader()) { + for (Map.Entry entry : request.GetHeader().entrySet()) { + callerHeaders.add(entry.getKey(), entry.getValue()); + } + } + RequestBuilder rb = RequestBuilder.create() + .fromClient(this) + .withSignMethod(request.getSkipSign() ? "SKIP" : ClientProfile.SIGN_TC3_256) + .withURL(HttpUrl.parse(url)) + .withHost(endpoint) + .withMethod(httpRequestMethod) + .withContentType(contentType) + .withPayload(request) + .withHeaders(callerHeaders.build()) + .withAction(action) + .withVersion(this.apiVersion) + .withRegion(this.region) + .withRequestClient(SDK_VERSION); + return this.httpConnection.doRequest(rb.build()); + } + // Construct the canonical request for signature calculation. + String canonicalUri = "/"; + String canonicalQueryString = this.getCanonicalQueryString(params, httpRequestMethod); + String canonicalHeaders = "content-type:" + contentType + "\nhost:" + endpoint + "\n"; + String signedHeaders = "content-type;host"; + + String hashedRequestPayload = ""; + if (this.profile.isUnsignedPayload()) { + hashedRequestPayload = Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8)); + } else { + hashedRequestPayload = Sign.sha256Hex(requestPayload); + } + String canonicalRequest = + httpRequestMethod + + "\n" + + canonicalUri + + "\n" + + canonicalQueryString + + "\n" + + canonicalHeaders + + "\n" + + signedHeaders + + "\n" + + hashedRequestPayload; + + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); + String service = endpoint.split("\\.")[0]; + String credentialScope = date + "/" + service + "/" + "tc3_request"; + String hashedCanonicalRequest = + Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); + String stringToSign = + "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest; + boolean skipSign = request.getSkipSign(); + String authorization = ""; + if (skipSign) { + authorization = "SKIP"; + } else { + String secretId = credSnapshot.getSecretId(); + String secretKey = credSnapshot.getSecretKey(); + byte[] secretDate = Sign.hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date); + byte[] secretService = Sign.hmac256(secretDate, service); + byte[] secretSigning = Sign.hmac256(secretService, "tc3_request"); + String signature = + DatatypeConverter.printHexBinary(Sign.hmac256(secretSigning, stringToSign)).toLowerCase(); + authorization = + "TC3-HMAC-SHA256 " + + "Credential=" + + secretId + + "/" + + credentialScope + + ", " + + "SignedHeaders=" + + signedHeaders + + ", " + + "Signature=" + + signature; + } + Builder hb = new Headers.Builder(); + hb.add("Content-Type", contentType) + .add("Host", endpoint) + .add("Authorization", authorization) + .add("X-TC-Action", action) + .add("X-TC-Timestamp", timestamp) + .add("X-TC-Version", this.apiVersion) + .add("X-TC-RequestClient", SDK_VERSION); + if (null != request.GetHeader()) { + for (Map.Entry entry : request.GetHeader().entrySet()) { + hb.add(entry.getKey(), entry.getValue()); + } + } + if (null != this.getRegion()) { + hb.add("X-TC-Region", this.getRegion()); + } + String token = credSnapshot.getToken(); + if (token != null && !token.isEmpty()) { + hb.add("X-TC-Token", token); + } + if (this.profile.isUnsignedPayload()) { + hb.add("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD"); + } + if (null != this.profile.getLanguage()) { + hb.add("X-TC-Language", this.profile.getLanguage().getValue()); + } + + String protocol = this.profile.getHttpProfile().getProtocol(); + String url = protocol + endpoint + this.path; + String apigwEndpoint = this.profile.getHttpProfile().getApigwEndpoint(); + if (null != apigwEndpoint) { + url = protocol + apigwEndpoint; + } + // Tag the outgoing multipart request with a RequestBuilder so failover + // can re-sign for an alternate host. Multipart body is treated as + // octet-stream for re-signing purposes (raw bytes, content-type preserved + // via the Content-Type header). + Headers.Builder callerHeaders = new Headers.Builder(); + if (null != request.GetHeader()) { + for (Map.Entry entry : request.GetHeader().entrySet()) { + callerHeaders.add(entry.getKey(), entry.getValue()); + } + } + RequestBuilder rb = RequestBuilder.create() + .fromClient(this) + .withSignMethod(request.getSkipSign() ? "SKIP" : ClientProfile.SIGN_TC3_256) + .withURL(HttpUrl.parse(url)) + .withHost(null != apigwEndpoint ? apigwEndpoint : endpoint) + .withMethod(httpRequestMethod) + .withContentTypeOctetStream() + .withPayload(requestPayload) + .withHeaders(callerHeaders.build()) + .withAction(action) + .withVersion(this.apiVersion) + .withRegion(this.region) + .withRequestClient(SDK_VERSION); + Request.Builder overrideBuilder = rb.build().newBuilder() + .header("Content-Type", contentType); + return this.httpConnection.doRequest(overrideBuilder.build()); + } + + /** + * Constructs the multipart payload for file uploads. + * + * @param request The request object containing file parameters. + * @param boundary The boundary string to separate parts of the multipart data. + * @return The byte array representing the multipart payload. + * @throws Exception If an error occurs during payload construction. + */ + private byte[] getMultipartPayload(AbstractModel request, String boundary) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + String[] binaryParams = request.getBinaryParams(); + // Iterate through each parameter in the multipart request. + for (Map.Entry entry : request.getMultipartRequestParams().entrySet()) { + baos.write("--".getBytes(StandardCharsets.UTF_8)); + baos.write(boundary.getBytes(StandardCharsets.UTF_8)); + baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); + baos.write("Content-Disposition: form-data; name=\"".getBytes(StandardCharsets.UTF_8)); + baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8)); + if (Arrays.asList(binaryParams).contains(entry.getKey())) { + baos.write("\"; filename=\"".getBytes(StandardCharsets.UTF_8)); + baos.write(entry.getKey().getBytes(StandardCharsets.UTF_8)); + baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8)); + } else { + baos.write("\"\r\n".getBytes(StandardCharsets.UTF_8)); + } + baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); + baos.write(entry.getValue()); + baos.write("\r\n".getBytes(StandardCharsets.UTF_8)); + } + // Write the closing boundary if there's any data. + if (baos.size() != 0) { + baos.write("--".getBytes(StandardCharsets.UTF_8)); + baos.write(boundary.getBytes(StandardCharsets.UTF_8)); + baos.write("--\r\n".getBytes(StandardCharsets.UTF_8)); + } + byte[] bytes = baos.toByteArray(); + baos.close(); + return bytes; + } + + /** + * Generates the canonical query string for GET requests. + * + * @param params The map of request parameters. + * @param method The HTTP method (should be GET). + * @return The canonical query string. + * @throws TencentCloudSDKException If UTF-8 encoding is not supported. + */ + private String getCanonicalQueryString(HashMap params, String method) + throws TencentCloudSDKException { + // POST requests don't have a query string in the signature. + if (method != null && method.equals(HttpProfile.REQ_POST)) { + return ""; + } + StringBuilder queryString = new StringBuilder(""); + // Iterate through each parameter and build the query string. + for (Map.Entry entry : params.entrySet()) { + String v; + try { + v = URLEncoder.encode(entry.getValue(), "UTF8"); + } catch (UnsupportedEncodingException e) { + throw new TencentCloudSDKException("UTF8 is not supported.", e); + } + queryString.append("&").append(entry.getKey()).append("=").append(v); + } + // Remove the leading '&' if the query string is not empty. + if (queryString.length() == 0) { + return ""; + } else { + return queryString.toString().substring(1); + } + } + + /** + * Formats the request data for signing (older signature methods). + * + * @param action The API action name. + * @param param The map of request parameters. + * @return The formatted string for signing. + * @throws TencentCloudSDKException If UTF-8 encoding is not supported. + */ + private String formatRequestData(String action, Map param) + throws TencentCloudSDKException { + Credential credSnapshot = this.credential.getSnapshot(); + String secretId = credSnapshot.getSecretId(); + String secretKey = credSnapshot.getSecretKey(); + String token = credSnapshot.getToken(); + param.put("Action", action); + param.put("RequestClient", this.sdkVersion); + param.put("Nonce", String.valueOf(Math.abs(new SecureRandom().nextInt()))); + param.put("Timestamp", String.valueOf(System.currentTimeMillis() / 1000)); + param.put("Version", this.apiVersion); + + // Add SecretId, Region, SignatureMethod, and Token if available. + if (secretId != null && (!secretId.isEmpty())) { + param.put("SecretId", secretId); + } + + if (this.region != null && (!this.region.isEmpty())) { + param.put("Region", this.region); + } + + if (this.profile.getSignMethod() != null && (!this.profile.getSignMethod().isEmpty())) { + param.put("SignatureMethod", this.profile.getSignMethod()); + } + + if (token != null && (!token.isEmpty())) { + param.put("Token", token); + } + + if (null != this.profile.getLanguage()) { + param.put("Language", this.profile.getLanguage().getValue()); + } + + String endpoint = this.getEndpoint(); + + // Generate the string to be signed. + String sigInParam = + Sign.makeSignPlainText( + new TreeMap(param), + this.profile.getHttpProfile().getReqMethod(), + endpoint, + this.path); + // Generate the signature. + String sigOutParam = + Sign.sign(secretKey, sigInParam, this.profile.getSignMethod()); + + String strParam = ""; + try { + // URL-encode each parameter and construct the query string. + for (Map.Entry entry : param.entrySet()) { + strParam += + (URLEncoder.encode(entry.getKey(), "utf-8") + + "=" + + URLEncoder.encode(entry.getValue(), "utf-8") + + "&"); + } + strParam += ("Signature=" + URLEncoder.encode(sigOutParam, "utf-8")); + } catch (UnsupportedEncodingException e) { + throw new TencentCloudSDKException("", e); + } + return strParam; + } + + /** + * Performs initializations to avoid performance costs in the first request. + */ + private void warmup() { + try { + // Initialize Mac instances (used for signature calculation). + // First invoke costs around 250 ms. + Mac.getInstance("HmacSHA1"); + Mac.getInstance("HmacSHA256"); + // Initialize SSLContext (used for HTTPS connections). + // First invoke costs around 150 ms. + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + } catch (Exception e) { + // Ignore but print the stack trace to the console for debugging. + e.printStackTrace(); + } + } + + /** + * Gets the API endpoint. + * + * @return The API endpoint URL. + */ + private String getEndpoint() { + // Use the endpoint from the profile if it's set, otherwise construct it from service and domain. + if (null != this.profile.getHttpProfile().getEndpoint()) { + return this.profile.getHttpProfile().getEndpoint(); + } else { + // protected abstract String getService(); + // use this.getService() from overrided subclass will be better + return this.service + "." + this.profile.getHttpProfile().getRootDomain(); + } + } + + String getServiceNameForFailover() { + return this.service; + } + + /** + * 请注意购买类接口谨慎调用,可能导致多次购买 + * 仅幂等接口推荐使用 + * + * @param req + * @param retryTimes + * @throws TencentCloudSDKException + */ + public Object retry(AbstractModel req, int retryTimes) throws TencentCloudSDKException { + if (retryTimes < 0 || retryTimes > 10) { + throw new TencentCloudSDKException("The number of retryTimes supported is 0 to 10.", "", "ClientSideError"); + } + Class cls = this.getClass(); + String methodName = req.getClass().getSimpleName().replace("Request", ""); + Method method; + try { + method = cls.getMethod(methodName, req.getClass()); + } catch (NoSuchMethodException e) { + throw new TencentCloudSDKException("ClientSideError", e); + } + do { + try { + return method.invoke(this, req); + } catch (IllegalAccessException e) { + throw new TencentCloudSDKException("ClientSideError", e); + } catch (InvocationTargetException e) { + if (retryTimes == 0) { + throw (TencentCloudSDKException) e.getTargetException(); + } + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new TencentCloudSDKException("ClientSideError", e); + } + } while (--retryTimes >= 0); + return null; + } + + /** + * Returns the circuit breaker previously set by + * {@link #setRegionBreaker(CircuitBreaker)}. + * + * @return The circuit breaker, or null if none was set. + */ + public CircuitBreaker getRegionBreaker() { + EndpointFailoverInterceptor interceptor = getFailoverInterceptor(); + return interceptor != null ? interceptor.getRegionBreaker() : null; + } + + /** + * Sets the circuit breaker to use for endpoint failover. The breaker's + * settings (maxFailNum, maxFailPercentage, windowIntervalMs, timeoutMs) + * are copied and applied to all per-host breakers created by the + * {@link EndpointFailoverInterceptor}. + * + * @param regionBreaker The circuit breaker whose settings will be used for failover. + */ + public void setRegionBreaker(CircuitBreaker regionBreaker) { + EndpointFailoverInterceptor interceptor = getFailoverInterceptor(); + if (interceptor != null) { + interceptor.setRegionBreaker(regionBreaker); + } + } + + private EndpointFailoverInterceptor getFailoverInterceptor() { + for (okhttp3.Interceptor i : this.httpConnection.getInterceptors()) { + if (i instanceof EndpointFailoverInterceptor) { + return (EndpointFailoverInterceptor) i; + } + } + return null; + } + +} diff --git a/src/main/java/com/tencentcloudapi/common/AbstractModel.java b/src/main/java/com/tencentcloudapi/common/AbstractModel.java index 638ad0b71..31d2cfb7e 100644 --- a/src/main/java/com/tencentcloudapi/common/AbstractModel.java +++ b/src/main/java/com/tencentcloudapi/common/AbstractModel.java @@ -1,274 +1,274 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; - -/** - * Abstract base class for all model objects in the Tencent Cloud SDK. - *

This class provides utility methods for serializing to and deserializing from JSON, - * managing customized parameters, and handling HTTP headers and requests.

- */ -public abstract class AbstractModel { - - /** - * Stores HTTP headers for the request. - */ - public Map header = new HashMap(); - - /** - * Flag to indicate whether to skip signing for this request. Defaults to false. - */ - protected boolean skipSign = false; - - /** - * Stores any custom parameters that are not part of the predefined model fields. - * These can be added dynamically and should be serializable to JSON. - */ - private HashMap customizedParams = new HashMap(); - - /** - * Convert an object of type O to its JSON string representation. - * - * @param obj The object to be serialized to JSON. - * @param The type of the object. - * @return A JSON string representation of the object. - */ - public static String toJsonString(O obj) { - return toJsonObject(obj).toString(); - } - - /** - * Recursively generates a JSON object from the model object. - * This method handles serialization for both custom parameters and regular fields. - * - * @param obj The object to be serialized. - * @param The type of the object. - * @return A JSON object representation of the model. - */ - private static JsonObject toJsonObject(O obj) { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - JsonObject joall = new JsonObject(); - - // Serialize customized parameters - JsonObject joadd = gson.toJsonTree(obj.any()).getAsJsonObject(); - for (Map.Entry entry : joadd.entrySet()) { - joall.add(entry.getKey(), entry.getValue()); - } - - // Serialize regular fields of the object - JsonObject jopublic = gson.toJsonTree(obj).getAsJsonObject(); - for (Map.Entry entry : jopublic.entrySet()) { - Object fo = null; - try { - // Access each field using reflection - Field f = obj.getClass().getDeclaredField(entry.getKey()); - f.setAccessible(true); - fo = f.get(obj); - } catch (Exception e) { - // This should never happen - e.printStackTrace(); - } - if (fo instanceof AbstractModel) { - // If the field is an AbstractModel, recursively serialize it - joall.add(entry.getKey(), toJsonObject((AbstractModel) fo)); - } else { - // Otherwise, directly add the value to the JSON object - joall.add(entry.getKey(), entry.getValue()); - } - } - return joall; - } - - /** - * Deserialize a JSON string into an object of a subclass of AbstractModel. - * - * @param json The JSON string to be deserialized. - * @param cls The class of the target model type. - * @param The type of the object to deserialize. - * @return An object of type O. - */ - public static O fromJsonString(String json, Class cls) { - Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); - return gson.fromJson(json, cls); - } - - /** - * Abstract method to be implemented by subclasses. - * Used to convert the model into a map for HTTP requests. - * - * @param map The map to populate with the model's parameters. - * @param prefix A string prefix for the keys. - */ - protected abstract void toMap(HashMap map, String prefix); - - /** - * Mark which parameters are binary type (for multipart requests). - * - * @return An array of parameter names that are binary. - */ - protected String[] getBinaryParams() { - return new String[0]; - } - - /** - * Return the multipart request parameters (if any). - * - * @return A map of multipart parameters. - */ - protected HashMap getMultipartRequestParams() { - return new HashMap(); - } - - /** - * Helper method to set a simple parameter in the map. - * Converts the parameter name and value to a string. - * - * @param map The map to put the parameter into. - * @param key The key for the parameter. - * @param value The value of the parameter. - * @param The type of the value. - */ - protected void setParamSimple(HashMap map, String key, V value) { - if (value != null) { - key = key.substring(0, 1).toUpperCase() + key.substring(1); - key = key.replace("_", "."); - map.put(key, String.valueOf(value)); - } - } - - /** - * Helper method to set an array of simple parameters in the map. - * - * @param map The map to put the parameters into. - * @param prefix The prefix for each key. - * @param array The array of values. - * @param The type of the values in the array. - */ - protected void setParamArraySimple(HashMap map, String prefix, V[] array) { - if (array != null) { - for (int i = 0; i < array.length; i++) { - this.setParamSimple(map, prefix + i, array[i]); - } - } - } - - /** - * Helper method to set an object parameter in the map. - * Recursively calls toMap() for complex model objects. - * - * @param map The map to put the parameter into. - * @param prefix The prefix for the key. - * @param obj The object to add to the map. - * @param The type of the object. - */ - protected void setParamObj( - HashMap map, String prefix, V obj) { - if (obj != null) { - obj.toMap(map, prefix); - } - } - - /** - * Helper method to set an array of object parameters in the map. - * - * @param map The map to put the parameters into. - * @param prefix The prefix for each key. - * @param array The array of object values. - * @param The type of the objects in the array. - */ - protected void setParamArrayObj( - HashMap map, String prefix, V[] array) { - if (array != null) { - for (int i = 0; i < array.length; i++) { - this.setParamObj(map, prefix + i + ".", array[i]); - } - } - } - - /** - * Set any custom key-value pair to this model. - * - * @param key The key to set. - * @param value The value to set. - */ - public void set(String key, Object value) { - this.customizedParams.put(key, value); - } - - /** - * Get all custom key-value pairs from this model. - * - * @return A map of custom parameters. - */ - public HashMap any() { - return this.customizedParams; - } - - /** - * Get the flag indicating whether the sign should be skipped. - * - * @return True if the sign should be skipped; otherwise, false. - */ - public boolean getSkipSign() { - return skipSign; - } - - /** - * Set the flag indicating whether the sign should be skipped. - * - * @param skipSign True to skip the sign; false otherwise. - */ - public void setSkipSign(boolean skipSign) { - this.skipSign = skipSign; - } - - /** - * Get the HTTP request headers. - * - * @return A map of HTTP headers. - */ - public Map GetHeader() { - return header; - } - - /** - * Set the HTTP request headers. - * - * @param header A map of HTTP headers to set. - */ - public void SetHeader(Map header) { - this.header = header; - } - - /** - * Check if the model is a stream type. - * - * @return False, as this model is not a stream type. - */ - public boolean isStream() { - return false; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +/** + * Abstract base class for all model objects in the Tencent Cloud SDK. + *

This class provides utility methods for serializing to and deserializing from JSON, + * managing customized parameters, and handling HTTP headers and requests.

+ */ +public abstract class AbstractModel { + + /** + * Stores HTTP headers for the request. + */ + public Map header = new HashMap(); + + /** + * Flag to indicate whether to skip signing for this request. Defaults to false. + */ + protected boolean skipSign = false; + + /** + * Stores any custom parameters that are not part of the predefined model fields. + * These can be added dynamically and should be serializable to JSON. + */ + private HashMap customizedParams = new HashMap(); + + /** + * Convert an object of type O to its JSON string representation. + * + * @param obj The object to be serialized to JSON. + * @param The type of the object. + * @return A JSON string representation of the object. + */ + public static String toJsonString(O obj) { + return toJsonObject(obj).toString(); + } + + /** + * Recursively generates a JSON object from the model object. + * This method handles serialization for both custom parameters and regular fields. + * + * @param obj The object to be serialized. + * @param The type of the object. + * @return A JSON object representation of the model. + */ + private static JsonObject toJsonObject(O obj) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + JsonObject joall = new JsonObject(); + + // Serialize customized parameters + JsonObject joadd = gson.toJsonTree(obj.any()).getAsJsonObject(); + for (Map.Entry entry : joadd.entrySet()) { + joall.add(entry.getKey(), entry.getValue()); + } + + // Serialize regular fields of the object + JsonObject jopublic = gson.toJsonTree(obj).getAsJsonObject(); + for (Map.Entry entry : jopublic.entrySet()) { + Object fo = null; + try { + // Access each field using reflection + Field f = obj.getClass().getDeclaredField(entry.getKey()); + f.setAccessible(true); + fo = f.get(obj); + } catch (Exception e) { + // This should never happen + e.printStackTrace(); + } + if (fo instanceof AbstractModel) { + // If the field is an AbstractModel, recursively serialize it + joall.add(entry.getKey(), toJsonObject((AbstractModel) fo)); + } else { + // Otherwise, directly add the value to the JSON object + joall.add(entry.getKey(), entry.getValue()); + } + } + return joall; + } + + /** + * Deserialize a JSON string into an object of a subclass of AbstractModel. + * + * @param json The JSON string to be deserialized. + * @param cls The class of the target model type. + * @param The type of the object to deserialize. + * @return An object of type O. + */ + public static O fromJsonString(String json, Class cls) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + return gson.fromJson(json, cls); + } + + /** + * Abstract method to be implemented by subclasses. + * Used to convert the model into a map for HTTP requests. + * + * @param map The map to populate with the model's parameters. + * @param prefix A string prefix for the keys. + */ + protected abstract void toMap(HashMap map, String prefix); + + /** + * Mark which parameters are binary type (for multipart requests). + * + * @return An array of parameter names that are binary. + */ + protected String[] getBinaryParams() { + return new String[0]; + } + + /** + * Return the multipart request parameters (if any). + * + * @return A map of multipart parameters. + */ + protected HashMap getMultipartRequestParams() { + return new HashMap(); + } + + /** + * Helper method to set a simple parameter in the map. + * Converts the parameter name and value to a string. + * + * @param map The map to put the parameter into. + * @param key The key for the parameter. + * @param value The value of the parameter. + * @param The type of the value. + */ + protected void setParamSimple(HashMap map, String key, V value) { + if (value != null) { + key = key.substring(0, 1).toUpperCase() + key.substring(1); + key = key.replace("_", "."); + map.put(key, String.valueOf(value)); + } + } + + /** + * Helper method to set an array of simple parameters in the map. + * + * @param map The map to put the parameters into. + * @param prefix The prefix for each key. + * @param array The array of values. + * @param The type of the values in the array. + */ + protected void setParamArraySimple(HashMap map, String prefix, V[] array) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + this.setParamSimple(map, prefix + i, array[i]); + } + } + } + + /** + * Helper method to set an object parameter in the map. + * Recursively calls toMap() for complex model objects. + * + * @param map The map to put the parameter into. + * @param prefix The prefix for the key. + * @param obj The object to add to the map. + * @param The type of the object. + */ + protected void setParamObj( + HashMap map, String prefix, V obj) { + if (obj != null) { + obj.toMap(map, prefix); + } + } + + /** + * Helper method to set an array of object parameters in the map. + * + * @param map The map to put the parameters into. + * @param prefix The prefix for each key. + * @param array The array of object values. + * @param The type of the objects in the array. + */ + protected void setParamArrayObj( + HashMap map, String prefix, V[] array) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + this.setParamObj(map, prefix + i + ".", array[i]); + } + } + } + + /** + * Set any custom key-value pair to this model. + * + * @param key The key to set. + * @param value The value to set. + */ + public void set(String key, Object value) { + this.customizedParams.put(key, value); + } + + /** + * Get all custom key-value pairs from this model. + * + * @return A map of custom parameters. + */ + public HashMap any() { + return this.customizedParams; + } + + /** + * Get the flag indicating whether the sign should be skipped. + * + * @return True if the sign should be skipped; otherwise, false. + */ + public boolean getSkipSign() { + return skipSign; + } + + /** + * Set the flag indicating whether the sign should be skipped. + * + * @param skipSign True to skip the sign; false otherwise. + */ + public void setSkipSign(boolean skipSign) { + this.skipSign = skipSign; + } + + /** + * Get the HTTP request headers. + * + * @return A map of HTTP headers. + */ + public Map GetHeader() { + return header; + } + + /** + * Set the HTTP request headers. + * + * @param header A map of HTTP headers to set. + */ + public void SetHeader(Map header) { + this.header = header; + } + + /** + * Check if the model is a stream type. + * + * @return False, as this model is not a stream type. + */ + public boolean isStream() { + return false; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java b/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java index 344d7a9ea..e01b1678b 100644 --- a/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java +++ b/src/main/java/com/tencentcloudapi/common/CircuitBreaker.java @@ -80,6 +80,15 @@ public CircuitBreaker(Setting setting) { this.setting = setting; } + /** + * Returns the settings used by this circuit breaker. + * + * @return The Setting instance. + */ + public Setting getSetting() { + return setting; + } + /** * Attempt to allow a request based on the current state of the circuit breaker. * @@ -214,7 +223,7 @@ private void onFailure(State state, long now) { all++; failures++; consecutiveSuccesses = 0; - consecutiveFailures = 0; + consecutiveFailures++; if (readyToOpen()) { setState(State.Open, now); } diff --git a/src/main/java/com/tencentcloudapi/common/Credential.java b/src/main/java/com/tencentcloudapi/common/Credential.java index f18560edd..bfdfc1943 100644 --- a/src/main/java/com/tencentcloudapi/common/Credential.java +++ b/src/main/java/com/tencentcloudapi/common/Credential.java @@ -1,196 +1,196 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.tencentcloudapi.common.exception.TencentCloudSDKException; - -/** - * Credential has many types in Tencent Cloud Access Management service. - * - *

We mainly use two of them: - * - *

Permanent credential: SecretId & SecretKey, can only be obtained from Tencent Cloud Management - * Console, https://console.cloud.tencent.com/cam/capi. - * - *

Ephemeral credential, can be obtained from Security Token Service (STS), has three dimensions: - * SecretId, SecretKey and Token. It will expire after a short time, hence you need to invoke STS - * API to refresh it. - * - *

Thread-safety and atomicity. The triple (secretId, secretKey, token) is only guaranteed - * to be read atomically via {@link #getSnapshot()}. The individual {@code getSecretId()}, - * {@code getSecretKey()}, {@code getToken()} getters do NOT guarantee that two consecutive calls - * observe a consistent triple — a refresh triggered between them may yield an id/key/token - * mismatch. Any code path that consumes more than one of the three fields together (e.g. request - * signing) should use {@link #getSnapshot()}. - */ -public class Credential { - private String secretId; - private String secretKey; - private String token; - private Updater updater; - - public Credential() { - } - - public Credential(String secretId, String secretKey) { - this(secretId, secretKey, ""); - } - - public Credential(String secretId, String secretKey, String token) { - this.secretId = secretId; - this.secretKey = secretKey; - this.token = token; - } - - public Credential(String secretId, String secretKey, String token, Updater updater) { - this.secretId = secretId; - this.secretKey = secretKey; - this.token = token; - this.updater = updater; - } - - public Updater getUpdater() { - return updater; - } - - public void setUpdater(Updater updater) { - this.updater = updater; - } - - /** - * Returns the secret id, triggering a refresh via the attached {@link Updater} if one is set. - * - *

Backward-compatibility note. This getter calls {@link #tryUpdate()} outside of any - * synchronization block. Two consequences follow from this: - *

    - *
  1. Concurrent threads can each enter {@code tryUpdate()} simultaneously, so the - * {@link Updater} may be invoked concurrently; well-behaved Updater implementations must - * guard against this themselves (e.g. via {@code needRefresh()} checks).
  2. - *
  3. An {@link Updater} that calls back into {@code getSecretId()}, {@code getSecretKey()}, - * or {@code getToken()} on the same credential will recurse infinitely. Use - * {@link #setCredential} for writes inside {@code update()}, never the individual - * getters.
  4. - *
- * Both behaviours are retained for backward compatibility. For any code path that reads more - * than one of the three fields together, use {@link #getSnapshot()} instead. - */ - public String getSecretId() { - tryUpdate(); - return this.secretId; - } - - public void setSecretId(String secretId) { - this.secretId = secretId; - } - - /** - * Returns the secret key, triggering a refresh via the attached {@link Updater} if one is set. - * - *

See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three - * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together. - */ - public String getSecretKey() { - tryUpdate(); - return this.secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - - /** - * Returns the session token, triggering a refresh via the attached {@link Updater} if one is - * set. - * - *

See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three - * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together. - */ - public String getToken() { - tryUpdate(); - return this.token; - } - - public void setToken(String token) { - this.token = token; - } - - /** - * Atomically replaces the entire (secretId, secretKey, token) triple under the same monitor - * used by {@link #getSnapshot()}. - * - *

Use this instead of three separate {@code setSecretId} / {@code setSecretKey} / - * {@code setToken} calls whenever you need to publish a freshly-refreshed triple: concurrent - * readers going through {@link #getSnapshot()} are guaranteed to observe either the old triple - * or the new triple in its entirety, never a mix of the two. The individual setters do not - * provide this guarantee and should only be used when no other thread is reading. - * - *

This method is the intended write-side companion to {@link #getSnapshot()}'s read-side - * atomicity. {@link Updater} implementations that refresh the triple in place (rather than - * constructing a new {@code Credential}) should call this method instead of the individual - * setters. - * - * @param secretId the new secret id. - * @param secretKey the new secret key. - * @param token the new token, may be empty or {@code null} for permanent credentials. - */ - public void setCredential(String secretId, String secretKey, String token) { - synchronized (this) { - this.secretId = secretId; - this.secretKey = secretKey; - this.token = token; - } - } - - /** - * Returns a point-in-time, self-consistent copy of the (secretId, secretKey, token) triple. - * - *

This is the only thread-safe, atomic way to read the credential triple. The refresh hook - * (if any) is invoked exactly once under a lock, and the three fields are then sampled together - * into a new {@code Credential} that does not carry an {@link Updater}. Use this in any code - * path that consumes more than one of the three fields together (e.g. request signing). - * - *

The returned object should be treated as read-only. It is a fresh instance and mutating it - * via the setters has no effect on the source credential, but doing so will break the - * consistency guarantee for the holder of the snapshot. - * - * @return a point-in-time copy of the credential triple, with no attached updater. - */ - public Credential getSnapshot() { - synchronized (this) { - tryUpdate(); - return new Credential(secretId, secretKey, token); - } - } - - private void tryUpdate() { - if (updater == null) { - return; - } - - try { - updater.update(this); - } catch (TencentCloudSDKException e) { - // wrap as RuntimeException to keep API consistent - throw new RuntimeException(e); - } - } - - public interface Updater { - void update(Credential credential) throws TencentCloudSDKException; - } -} +/* + * Copyright (c) 2018 THL A29 Limited, a Tencent company. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; + +/** + * Credential has many types in Tencent Cloud Access Management service. + * + *

We mainly use two of them: + * + *

Permanent credential: SecretId & SecretKey, can only be obtained from Tencent Cloud Management + * Console, https://console.cloud.tencent.com/cam/capi. + * + *

Ephemeral credential, can be obtained from Security Token Service (STS), has three dimensions: + * SecretId, SecretKey and Token. It will expire after a short time, hence you need to invoke STS + * API to refresh it. + * + *

Thread-safety and atomicity. The triple (secretId, secretKey, token) is only guaranteed + * to be read atomically via {@link #getSnapshot()}. The individual {@code getSecretId()}, + * {@code getSecretKey()}, {@code getToken()} getters do NOT guarantee that two consecutive calls + * observe a consistent triple — a refresh triggered between them may yield an id/key/token + * mismatch. Any code path that consumes more than one of the three fields together (e.g. request + * signing) should use {@link #getSnapshot()}. + */ +public class Credential { + private String secretId; + private String secretKey; + private String token; + private Updater updater; + + public Credential() { + } + + public Credential(String secretId, String secretKey) { + this(secretId, secretKey, ""); + } + + public Credential(String secretId, String secretKey, String token) { + this.secretId = secretId; + this.secretKey = secretKey; + this.token = token; + } + + public Credential(String secretId, String secretKey, String token, Updater updater) { + this.secretId = secretId; + this.secretKey = secretKey; + this.token = token; + this.updater = updater; + } + + public Updater getUpdater() { + return updater; + } + + public void setUpdater(Updater updater) { + this.updater = updater; + } + + /** + * Returns the secret id, triggering a refresh via the attached {@link Updater} if one is set. + * + *

Backward-compatibility note. This getter calls {@link #tryUpdate()} outside of any + * synchronization block. Two consequences follow from this: + *

    + *
  1. Concurrent threads can each enter {@code tryUpdate()} simultaneously, so the + * {@link Updater} may be invoked concurrently; well-behaved Updater implementations must + * guard against this themselves (e.g. via {@code needRefresh()} checks).
  2. + *
  3. An {@link Updater} that calls back into {@code getSecretId()}, {@code getSecretKey()}, + * or {@code getToken()} on the same credential will recurse infinitely. Use + * {@link #setCredential} for writes inside {@code update()}, never the individual + * getters.
  4. + *
+ * Both behaviours are retained for backward compatibility. For any code path that reads more + * than one of the three fields together, use {@link #getSnapshot()} instead. + */ + public String getSecretId() { + tryUpdate(); + return this.secretId; + } + + public void setSecretId(String secretId) { + this.secretId = secretId; + } + + /** + * Returns the secret key, triggering a refresh via the attached {@link Updater} if one is set. + * + *

See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three + * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together. + */ + public String getSecretKey() { + tryUpdate(); + return this.secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + /** + * Returns the session token, triggering a refresh via the attached {@link Updater} if one is + * set. + * + *

See {@link #getSecretId()} for the backward-compatibility caveats that apply to all three + * individual getters. Prefer {@link #getSnapshot()} when reading multiple fields together. + */ + public String getToken() { + tryUpdate(); + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + + /** + * Atomically replaces the entire (secretId, secretKey, token) triple under the same monitor + * used by {@link #getSnapshot()}. + * + *

Use this instead of three separate {@code setSecretId} / {@code setSecretKey} / + * {@code setToken} calls whenever you need to publish a freshly-refreshed triple: concurrent + * readers going through {@link #getSnapshot()} are guaranteed to observe either the old triple + * or the new triple in its entirety, never a mix of the two. The individual setters do not + * provide this guarantee and should only be used when no other thread is reading. + * + *

This method is the intended write-side companion to {@link #getSnapshot()}'s read-side + * atomicity. {@link Updater} implementations that refresh the triple in place (rather than + * constructing a new {@code Credential}) should call this method instead of the individual + * setters. + * + * @param secretId the new secret id. + * @param secretKey the new secret key. + * @param token the new token, may be empty or {@code null} for permanent credentials. + */ + public void setCredential(String secretId, String secretKey, String token) { + synchronized (this) { + this.secretId = secretId; + this.secretKey = secretKey; + this.token = token; + } + } + + /** + * Returns a point-in-time, self-consistent copy of the (secretId, secretKey, token) triple. + * + *

This is the only thread-safe, atomic way to read the credential triple. The refresh hook + * (if any) is invoked exactly once under a lock, and the three fields are then sampled together + * into a new {@code Credential} that does not carry an {@link Updater}. Use this in any code + * path that consumes more than one of the three fields together (e.g. request signing). + * + *

The returned object should be treated as read-only. It is a fresh instance and mutating it + * via the setters has no effect on the source credential, but doing so will break the + * consistency guarantee for the holder of the snapshot. + * + * @return a point-in-time copy of the credential triple, with no attached updater. + */ + public Credential getSnapshot() { + synchronized (this) { + tryUpdate(); + return new Credential(secretId, secretKey, token); + } + } + + private void tryUpdate() { + if (updater == null) { + return; + } + + try { + updater.update(this); + } catch (TencentCloudSDKException e) { + // wrap as RuntimeException to keep API consistent + throw new RuntimeException(e); + } + } + + public interface Updater { + void update(Credential credential) throws TencentCloudSDKException; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java b/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java new file mode 100644 index 000000000..eb04f1a8b --- /dev/null +++ b/src/main/java/com/tencentcloudapi/common/EndpointFailoverInterceptor.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import okhttp3.*; + +import javax.net.ssl.SSLException; +import java.io.IOException; +import java.net.*; +import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Selects a healthy API host via per-host circuit breakers. + * No same-request retry — API calls may be non-idempotent. + * All breakers open → fall through to origin host. + */ +class EndpointFailoverInterceptor implements Interceptor { + + /** More-specific families first — matchFamily returns first hit. */ + static final String[][] FAILOVER_DOMAIN_FAMILIES = { + { + "ai.tencentcloudapi.com", + "ai.tencentcloudapi.com.cn", + "ai.tencentcloudapi.cn", + }, + { + "internal.tencentcloudapi.com", + "internal.tencentcloudapi.com.cn", + "internal.tencentcloudapi.cn", + }, + { + "tencentcloudapi.com", + "tencentcloudapi.com.cn", + "tencentcloudapi.cn", + }, + }; + + public static long BREAKER_TIMEOUT_MS = 60 * 1000; + + private final AbstractClient client; + private final String backupEndpoint; + // Per AbstractClient instance. + private final ConcurrentHashMap breakers = + new ConcurrentHashMap(); + + /** + * Stores the region breaker set via {@link AbstractClient#setRegionBreaker} + * so its settings are applied to all per-host breakers created here. + */ + private CircuitBreaker regionBreaker; + + EndpointFailoverInterceptor(AbstractClient client) { + this.client = client; + String bp = client.getClientProfile().getBackupEndpoint(); + this.backupEndpoint = (bp != null && !bp.isEmpty()) ? bp : null; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String originHost = request.url().host(); + + // selectHost always returns a non-null Candidate. + Candidate c = selectHost(request); + + try { + // rewriteFor returns the original request unchanged when c.host == originHost. + Request rewritten = rewriteFor(request, originHost, c.host); + Response raw = chain.proceed(rewritten); + Response validated = validateResponse(raw); + c.token.report(true); + return validated; + } catch (TencentCloudSDKException e) { + throw new IOException("Failed to re-sign request for failover: " + e.getMessage(), e); + } catch (IOException e) { + if (!shouldFailover(e)) { + throw e; + } + c.token.report(false); + throw e; + } + } + + // --- Candidate selection --- + + /** + * Returns the first candidate whose breaker is closed, or the origin host + * when all breakers are open. + */ + Candidate selectHost(Request request) { + String urlHost = request.url().host(); + + // Fast path: check the origin host's breaker first. In the common case + // the origin is healthy, so we avoid allocating the candidate list entirely. + CircuitBreaker.Token originToken = breakerFor(urlHost).allow(); + if (originToken.allowed) { + return new Candidate(urlHost, originToken); + } + + // Origin host is tripped — build the failover candidates (excluding the + // origin, which we already checked) and walk them. + List candidates = buildCandidateHosts(urlHost); + for (int i = 0; i < candidates.size(); i++) { + String host = candidates.get(i); + CircuitBreaker.Token token = breakerFor(host).allow(); + if (token.allowed) { + return new Candidate(host, token); + } + } + // All breakers open: fall back to origin host. + return new Candidate(urlHost, originToken); + } + + /** + * Ordered failover candidates (excluding the origin host, which is checked + * first in {@link #selectHost}). Priority: backup endpoint, then TLD-family rotation. + */ + private List buildCandidateHosts(String urlHost) { + String prefix = serviceOf(urlHost); + if (prefix == null || prefix.isEmpty()) { + return Collections.emptyList(); + } + + // 1. backup endpoint + if (backupEndpoint != null) { + return Collections.singletonList(prefix + "." + backupEndpoint); + } + + // 2. Default TLD-family rotation + Match match = matchFamily(urlHost); + if (match == null) { + return Collections.emptyList(); + } + List hosts = new ArrayList(); + for (int i = 1; i < match.family.length; i++) { + int tldIdx = (match.tldIdx + i) % match.family.length; + hosts.add(prefix + "." + match.family[tldIdx]); + } + return hosts; + } + + CircuitBreaker breakerFor(String host) { + CircuitBreaker existing = breakers.get(host); + if (existing != null) { + return existing; + } + CircuitBreaker created = newBreaker(); + CircuitBreaker prev = breakers.putIfAbsent(host, created); + return prev != null ? prev : created; + } + + /** Test hook. */ + void putBreakerForTesting(String host, CircuitBreaker breaker) { + breakers.put(host, breaker); + } + + private CircuitBreaker newBreaker() { + if (regionBreaker != null) { + return new CircuitBreaker(regionBreaker.getSetting()); + } + CircuitBreaker.Setting s = new CircuitBreaker.Setting(); + s.timeoutMs = BREAKER_TIMEOUT_MS; + return new CircuitBreaker(s); + } + + /** Package-private: store the region breaker instance. */ + void setRegionBreaker(CircuitBreaker regionBreaker) { + this.regionBreaker = regionBreaker; + } + + /** Package-private: return the region breaker instance. */ + CircuitBreaker getRegionBreaker() { + return regionBreaker; + } + + // Per-candidate helpers. + + private Request rewriteFor(Request request, String originHost, String targetHost) + throws TencentCloudSDKException, IOException { + if (originHost.equals(targetHost)) { + return request; + } + return RequestBuilder.from(request) + .fromClient(client) + .withUrlHost(targetHost) + .withUrlSchemeHttps() + .withHost(targetHost) + .build(); + } + + static final class Candidate { + final String host; + final CircuitBreaker.Token token; + + Candidate(String host, CircuitBreaker.Token token) { + this.host = host; + this.token = token; + } + } + + // --- Host classification & TLD family matching --- + + static boolean isKnownTencentCloudHost(String host) { + return matchFamily(host) != null; + } + + /** Returns null if host matches no family. */ + static Match matchFamily(String host) { + if (host == null) { + return null; + } + for (int familyIdx = 0; familyIdx < FAILOVER_DOMAIN_FAMILIES.length; familyIdx++) { + Match m = tryMatchFamily(host, FAILOVER_DOMAIN_FAMILIES[familyIdx]); + if (m != null) { + return m; + } + } + return null; + } + + private static Match tryMatchFamily(String host, String[] family) { + for (int tldIdx = 0; tldIdx < family.length; tldIdx++) { + String suffix = family[tldIdx]; + if (!host.endsWith("." + suffix)) { + continue; + } + String prefix = host.substring(0, host.length() - suffix.length() - 1); + if (prefix.isEmpty() || hasEmptyLabel(prefix)) { + return null; + } + return new Match(family, prefix, tldIdx); + } + return null; + } + + // "foo..bar" → malformed hostname. + private static boolean hasEmptyLabel(String prefix) { + if (prefix.startsWith(".") || prefix.endsWith(".")) { + return true; + } + for (int i = 0; i < prefix.length() - 1; i++) { + if (prefix.charAt(i) == '.' && prefix.charAt(i + 1) == '.') { + return true; + } + } + return false; + } + + static final class Match { + final String[] family; + final String prefix; + final int tldIdx; + + Match(String[] family, String prefix, int tldIdx) { + this.family = family; + this.prefix = prefix; + this.tldIdx = tldIdx; + } + } + + /** Test hook. */ + static String hostWithTld(String originHost, int newTldIdx) { + Match m = matchFamily(originHost); + return serviceOf(originHost) + "." + m.family[newTldIdx]; + } + + private static String serviceOf(String host) { + int dot = host.indexOf('.'); + return dot < 0 ? host : host.substring(0, dot); + } + + // Failure classification. + + // Host is unreachable or compromised — not a transient application error. + private static boolean shouldFailover(IOException e) { + return e instanceof UnknownHostException + || e instanceof SSLException + || e instanceof SocketException + || e instanceof SocketTimeoutException + || e instanceof UnhealthyResponseException; + } + + // Wraps non-200 or invalid-JSON responses so validateResponse's caller + // records them as breaker failures alongside transport errors. + private static final class UnhealthyResponseException extends IOException { + UnhealthyResponseException(String message) { + super(message); + } + } + + /** + * Validates response is healthy based solely on the HTTP status code. + * Body content is not inspected — business-level errors (HTTP 200 + JSON + * error body) are handled by the response parser, not by the failover + * breaker. Non-200 responses are recorded as breaker failures. + */ + private static Response validateResponse(Response resp) throws IOException { + if (resp.code() != 200) { + String msg = "HTTP " + resp.code() + " " + resp.message(); + resp.close(); + throw new UnhealthyResponseException(msg); + } + return resp; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java b/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java index 990270676..d72b2a5e0 100644 --- a/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java +++ b/src/main/java/com/tencentcloudapi/common/JsonResponseErrModel.java @@ -1,41 +1,41 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -public class JsonResponseErrModel { - @SerializedName("RequestId") - @Expose - public String requestId; - - @SerializedName("Error") - @Expose - public ErrorInfo error; - - class ErrorInfo { - @SerializedName("Code") - @Expose - public String code; - - @Expose - @SerializedName("Message") - public String message; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class JsonResponseErrModel { + @SerializedName("RequestId") + @Expose + public String requestId; + + @SerializedName("Error") + @Expose + public ErrorInfo error; + + class ErrorInfo { + @SerializedName("Code") + @Expose + public String code; + + @Expose + @SerializedName("Message") + public String message; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java b/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java index 54af3dcd2..8c10cef95 100644 --- a/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java +++ b/src/main/java/com/tencentcloudapi/common/JsonResponseModel.java @@ -1,27 +1,27 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -public class JsonResponseModel { - @SerializedName("Response") - @Expose - public T response; -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class JsonResponseModel { + @SerializedName("Response") + @Expose + public T response; +} diff --git a/src/main/java/com/tencentcloudapi/common/RequestBuilder.java b/src/main/java/com/tencentcloudapi/common/RequestBuilder.java new file mode 100644 index 000000000..47a2e7091 --- /dev/null +++ b/src/main/java/com/tencentcloudapi/common/RequestBuilder.java @@ -0,0 +1,715 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import okhttp3.*; +import okio.Buffer; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Holds all parameters for one cloud API call and builds a signed OkHttp {@link Request}. + * + *

Mirrors the Go SDK requestBuilder: {@code url} is the single source of truth for + * transport, {@code host} signs the canonical Host header. Failover swaps both + * {@code url.host} and {@code host}, then rebuilds.

+ */ +final class RequestBuilder { + static final String SIGN_SKIP = "SKIP"; + + private enum ContentType { + JSON("application/json"), + FORM("application/x-www-form-urlencoded"), + OCTET_STREAM("application/octet-stream"); + + private final String wire; + + ContentType(String wire) { + this.wire = wire; + } + + String wire() { + return wire; + } + } + + interface NowProvider { + long currentTimeMillis(); + } + + private static final NowProvider SYSTEM_NOW = new NowProvider() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + }; + + // API identity + private String service; + private String version; + private String action; + private String region; + + // Transport: single source of truth; resign only swaps url.host + private HttpUrl url; + // host signs the canonical Host header; may differ from url.host + private String host; + private String method; + + // Payload + private Object payload; + private ContentType contentType; + + // Caller headers (excluded from signing) + private Headers headers = new Headers.Builder().build(); + + // Signing + private Credential credential; + private String signMethod; + private boolean unsignedPayload; + private String language; + private String requestClient; + + private NowProvider now = SYSTEM_NOW; + private long nonce; + + static RequestBuilder create() { + return new RequestBuilder(); + } + + RequestBuilder(Request original) throws IOException, TencentCloudSDKException { + this.url = original.url(); + this.host = hostHeaderOf(original); + this.method = original.method(); + this.contentType = contentTypeFromHeader(original.header("Content-Type"), original.method()); + this.payload = payloadFromRequest(original, this.contentType); + this.headers = callerHeadersFrom(original); + this.action = original.header("X-TC-Action"); + this.version = original.header("X-TC-Version"); + this.region = original.header("X-TC-Region"); + this.language = original.header("X-TC-Language"); + this.requestClient = original.header("X-TC-RequestClient"); + this.unsignedPayload = "UNSIGNED-PAYLOAD".equals(original.header("X-TC-Content-SHA256")); + if (SIGN_SKIP.equals(original.header("Authorization"))) { + this.signMethod = SIGN_SKIP; + } + Map params = paramsIfAvailable(this.payload); + if (params != null) { + if (this.action == null) this.action = params.get("Action"); + if (this.version == null) this.version = params.get("Version"); + if (this.region == null) this.region = params.get("Region"); + if (this.requestClient == null) this.requestClient = params.get("RequestClient"); + } + } + + static RequestBuilder from(Request request) throws IOException, TencentCloudSDKException { + RequestBuilder tagged = request.tag(RequestBuilder.class); + return tagged == null ? new RequestBuilder(request) : tagged.copy(); + } + + RequestBuilder withCredential(Credential credential) { + this.credential = credential; + return this; + } + + RequestBuilder withSignMethod(String signMethod) { + this.signMethod = signMethod; + return this; + } + + RequestBuilder withUnsignedPayload(boolean unsignedPayload) { + this.unsignedPayload = unsignedPayload; + return this; + } + + RequestBuilder withHost(String host) { + this.host = host; + return this; + } + + /** Swaps url.host to targetHost (transport target), matching Go's rb.url.Host = targetHost. */ + RequestBuilder withUrlHost(String targetHost) { + if (this.url == null) { + throw new IllegalStateException("url must be set before withUrlHost"); + } + this.url = this.url.newBuilder().host(targetHost).build(); + return this; + } + + /** Forces url.scheme to https — failover endpoints are https-only. */ + RequestBuilder withUrlSchemeHttps() { + if (this.url == null) { + throw new IllegalStateException("url must be set before withUrlSchemeHttps"); + } + this.url = this.url.newBuilder().scheme("https").build(); + return this; + } + + /** Overrides the full transport target. */ + RequestBuilder withURL(URL url) { + this.url = HttpUrl.get(url); + return this; + } + + RequestBuilder withURL(HttpUrl url) { + this.url = url; + return this; + } + + RequestBuilder withMethod(String method) { + this.method = method; + return this; + } + + RequestBuilder withContentType(String headerValue) { + this.contentType = contentTypeFromHeader(headerValue, method); + return this; + } + + RequestBuilder withContentTypeJson() { + this.contentType = ContentType.JSON; + return this; + } + + RequestBuilder withContentTypeForm() { + this.contentType = ContentType.FORM; + return this; + } + + RequestBuilder withContentTypeOctetStream() { + this.contentType = ContentType.OCTET_STREAM; + return this; + } + + RequestBuilder withService(String service) { + this.service = service; + return this; + } + + RequestBuilder withVersion(String version) { + this.version = version; + return this; + } + + RequestBuilder withAction(String action) { + this.action = action; + return this; + } + + RequestBuilder withRegion(String region) { + this.region = region; + return this; + } + + RequestBuilder withPayload(Object payload) { + this.payload = payload; + return this; + } + + RequestBuilder withHeaders(Headers headers) { + this.headers = headers == null ? new Headers.Builder().build() : headers; + return this; + } + + RequestBuilder withLanguage(String language) { + this.language = language; + return this; + } + + RequestBuilder withRequestClient(String requestClient) { + this.requestClient = requestClient; + return this; + } + + RequestBuilder withNow(NowProvider now) { + this.now = now == null ? SYSTEM_NOW : now; + return this; + } + + RequestBuilder withNonce(long nonce) { + this.nonce = nonce; + return this; + } + + RequestBuilder fromClient(AbstractClient client) { + ClientProfile profile = client.getClientProfile(); + return withCredential(client.getCredential()) + .withSignMethod(this.signMethod != null ? this.signMethod : profile.getSignMethod()) + .withUnsignedPayload(profile.isUnsignedPayload() || this.unsignedPayload) + .withService(client.getServiceNameForFailover()) + .withRegion(client.getRegion()) + .withLanguage(profile.getLanguage() == null ? null : profile.getLanguage().getValue()) + .withRequestClient(this.requestClient != null ? this.requestClient : AbstractClient.SDK_VERSION); + } + + Request build() throws TencentCloudSDKException, IOException { + if (method == null) { + method = HttpProfile.REQ_POST; + } + if (contentType == null) { + contentType = HttpProfile.REQ_GET.equalsIgnoreCase(method) ? ContentType.FORM : ContentType.JSON; + } + if (signMethod == null) { + throw new TencentCloudSDKException("sign method must be set"); + } + if (!SIGN_SKIP.equals(signMethod) + && !ClientProfile.SIGN_TC3_256.equals(signMethod) + && !ClientProfile.SIGN_SHA256.equals(signMethod) + && !ClientProfile.SIGN_SHA1.equals(signMethod)) { + throw new TencentCloudSDKException( + "Signature method " + signMethod + " is invalid or not supported yet."); + } + if (!SIGN_SKIP.equals(signMethod) && credential == null) { + throw new TencentCloudSDKException("credential must be set"); + } + if (ClientProfile.SIGN_TC3_256.equals(signMethod)) { + return buildV3(); + } + if (ClientProfile.SIGN_SHA256.equals(signMethod) || ClientProfile.SIGN_SHA1.equals(signMethod)) { + return buildV1(signMethod); + } + return buildSkip(); + } + + // ---------- V3 (TC3-HMAC-SHA256) ---------- + + private Request buildV3() throws TencentCloudSDKException, IOException { + long millis = now.currentTimeMillis(); + String timestamp = String.valueOf(millis / 1000); + String date = utcDate(timestamp); + + BodyAndQuery bq = v3BodyAndQuery(); + String signingHost = resolvedHost(); + String serviceName = resolvedService(signingHost); + String ctStr = contentType.wire(); + + String canonicalUri = encodedPath(url); + String canonicalHeaders = "content-type:" + ctStr + "\nhost:" + signingHost + "\n"; + String signedHeaders = "content-type;host"; + String hashedRequestPayload = unsignedPayload + ? Sign.sha256Hex("UNSIGNED-PAYLOAD".getBytes(StandardCharsets.UTF_8)) + : Sign.sha256Hex(bq.body); + String canonicalRequest = method + "\n" + + canonicalUri + "\n" + + bq.query + "\n" + + canonicalHeaders + "\n" + + signedHeaders + "\n" + + hashedRequestPayload; + + String credentialScope = date + "/" + serviceName + "/tc3_request"; + String stringToSign = "TC3-HMAC-SHA256\n" + timestamp + "\n" + + credentialScope + "\n" + + Sign.sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8)); + + byte[] secretDate = Sign.hmac256( + ("TC3" + credential.getSecretKey()).getBytes(StandardCharsets.UTF_8), date); + byte[] secretService = Sign.hmac256(secretDate, serviceName); + byte[] secretSigning = Sign.hmac256(secretService, "tc3_request"); + String signature = DatatypeConverter + .printHexBinary(Sign.hmac256(secretSigning, stringToSign)) + .toLowerCase(); + String authorization = "TC3-HMAC-SHA256 " + + "Credential=" + credential.getSecretId() + "/" + credentialScope + ", " + + "SignedHeaders=" + signedHeaders + ", " + + "Signature=" + signature; + + Headers.Builder hb = signedHeaderBuilder(ctStr, signingHost, timestamp); + hb.set("Authorization", authorization); + addCallerHeaders(hb); + return newRequest(bq, hb.build()); + } + + // ---------- V1 (HmacSHA1 / HmacSHA256) ---------- + + private Request buildV1(String sm) throws TencentCloudSDKException, IOException { + if (!HttpProfile.REQ_GET.equalsIgnoreCase(method) && !HttpProfile.REQ_POST.equalsIgnoreCase(method)) { + throw new TencentCloudSDKException("Method only support (GET, POST) for Hmac sign"); + } + Map params = paramsFromPayload(); + putIfNotNull(params, "Action", action); + putIfNotNull(params, "Version", version); + putIfNotNull(params, "Region", region); + params.put("Timestamp", String.valueOf(now.currentTimeMillis() / 1000)); + params.put("Nonce", String.valueOf(nonce)); + putIfNotNull(params, "RequestClient", requestClient); + + String secretId = credential.getSecretId(); + String secretKey = credential.getSecretKey(); + String token = credential.getToken(); + if (secretId != null && !secretId.isEmpty()) { + params.put("SecretId", secretId); + } + params.put("SignatureMethod", sm); + if (token != null && !token.isEmpty()) { + params.put("Token", token); + } else { + params.remove("Token"); + } + params.remove("Signature"); + + String signingHost = resolvedHost(); + String stringToSign = Sign.makeSignPlainText( + new TreeMap(params), method, signingHost, encodedPath(url)); + String signature = Sign.sign(secretKey, stringToSign, sm); + params.put("Signature", signature); + + HttpUrl outUrl = url == null + ? new HttpUrl.Builder().scheme("https").host(signingHost).addPathSegment("").build() + : url; + Request.Builder rb = new Request.Builder(); + if (HttpProfile.REQ_GET.equalsIgnoreCase(method)) { + rb.url(outUrl.newBuilder().encodedQuery(encodedParams(params)).build()).get(); + } else { + rb.url(outUrl).post(RequestBody.create( + MediaType.parse(ContentType.FORM.wire()), encodedParams(params))); + } + Headers.Builder hb = new Headers.Builder(); + hb.set("Content-Type", ContentType.FORM.wire()); + hb.set("Host", signingHost); + addCallerHeaders(hb); + return rb.headers(hb.build()).tag(RequestBuilder.class, copy()).build(); + } + + // ---------- SKIP (no signature) ---------- + + private Request buildSkip() throws TencentCloudSDKException, IOException { + BodyAndQuery bq = v3BodyAndQuery(); + String signingHost = resolvedHost(); + Headers.Builder hb = signedHeaderBuilder(contentType.wire(), signingHost, + String.valueOf(now.currentTimeMillis() / 1000)); + hb.set("Authorization", SIGN_SKIP); + addCallerHeaders(hb); + return newRequest(bq, hb.build()); + } + + // ---------- Shared helpers ---------- + + private Headers.Builder signedHeaderBuilder(String ctStr, String signingHost, String timestamp) { + Headers.Builder hb = new Headers.Builder(); + hb.set("Content-Type", ctStr); + hb.set("Host", signingHost); + putHeaderIfNotEmpty(hb, "X-TC-Action", action); + putHeaderIfNotEmpty(hb, "X-TC-Version", version); + hb.set("X-TC-Timestamp", timestamp); + putHeaderIfNotEmpty(hb, "X-TC-RequestClient", requestClient); + putHeaderIfNotEmpty(hb, "X-TC-Language", language); + putHeaderIfNotEmpty(hb, "X-TC-Region", region); + if (credential != null) { + String token = credential.getToken(); + if (token != null && !token.isEmpty()) { + hb.set("X-TC-Token", token); + } + } + if (unsignedPayload) { + hb.set("X-TC-Content-SHA256", "UNSIGNED-PAYLOAD"); + } + return hb; + } + + private Request newRequest(BodyAndQuery bq, Headers headers) { + HttpUrl outUrl = url; + if (outUrl == null) { + String signingHost = resolvedHost(); + outUrl = new HttpUrl.Builder().scheme("https").host(signingHost).addPathSegment("").build(); + } + if (bq.query != null && !bq.query.isEmpty()) { + outUrl = outUrl.newBuilder().encodedQuery(bq.query).build(); + } + RequestBody body = null; + if (!HttpProfile.REQ_GET.equalsIgnoreCase(method) && bq.body.length > 0) { + body = RequestBody.create(MediaType.parse(contentType.wire()), bq.body); + } + Request.Builder rb = new Request.Builder().url(outUrl).headers(headers).tag(RequestBuilder.class, copy()); + if (HttpProfile.REQ_GET.equalsIgnoreCase(method)) { + rb.get(); + } else { + rb.method(method, body == null ? RequestBody.create(null, new byte[0]) : body); + } + return rb.build(); + } + + private BodyAndQuery v3BodyAndQuery() throws TencentCloudSDKException { + if (HttpProfile.REQ_GET.equalsIgnoreCase(method)) { + Map params = paramsFromPayload(); + for (String key : new String[]{"Action", "Version", "Region", "Timestamp", "Nonce", "RequestClient"}) { + params.remove(key); + } + return new BodyAndQuery(new byte[0], encodedParams(params)); + } + + switch (contentType) { + case JSON: + if (payload instanceof byte[]) { + return new BodyAndQuery((byte[]) payload, ""); + } + if (payload instanceof String) { + return new BodyAndQuery(((String) payload).getBytes(StandardCharsets.UTF_8), ""); + } + if (payload instanceof AbstractModel) { + return new BodyAndQuery(AbstractModel.toJsonString((AbstractModel) payload) + .getBytes(StandardCharsets.UTF_8), ""); + } + return new BodyAndQuery(String.valueOf(payload).getBytes(StandardCharsets.UTF_8), ""); + case FORM: + return new BodyAndQuery(encodedParams(paramsFromPayload()).getBytes(StandardCharsets.UTF_8), ""); + case OCTET_STREAM: + if (!(payload instanceof byte[])) { + throw new TencentCloudSDKException( + "octet-stream payload must be byte[], got " + payload.getClass().getName()); + } + return new BodyAndQuery((byte[]) payload, ""); + default: + throw new TencentCloudSDKException("unsupported content type"); + } + } + + private Map paramsFromPayload() throws TencentCloudSDKException { + Map existing = paramsIfAvailable(payload); + if (existing != null) { + return new LinkedHashMap(existing); + } + if (payload instanceof AbstractModel) { + HashMap params = new HashMap(); + ((AbstractModel) payload).toMap(params, ""); + return params; + } + if (payload instanceof byte[]) { + return decodeFormParams(new String((byte[]) payload, StandardCharsets.UTF_8)); + } + throw new TencentCloudSDKException("cannot derive params from payload type " + + (payload == null ? "null" : payload.getClass().getName())); + } + + @SuppressWarnings("unchecked") + private static Map paramsIfAvailable(Object payload) { + if (payload instanceof Map) { + Map in = (Map) payload; + LinkedHashMap out = new LinkedHashMap(); + for (Map.Entry e : in.entrySet()) { + out.put(String.valueOf(e.getKey()), e.getValue() == null ? "" : String.valueOf(e.getValue())); + } + return out; + } + return null; + } + + private String resolvedHost() { + return host != null && !host.isEmpty() ? host : (url != null ? url.host() : ""); + } + + private String resolvedService(String signingHost) { + if (service != null && !service.isEmpty()) { + return service; + } + int dot = signingHost.indexOf('.'); + return dot < 0 ? signingHost : signingHost.substring(0, dot); + } + + private RequestBuilder copy() { + RequestBuilder rb = new RequestBuilder(); + rb.service = service; + rb.version = version; + rb.action = action; + rb.region = region; + rb.url = url; + rb.host = host; + rb.method = method; + rb.payload = copyPayload(payload); + rb.contentType = contentType; + rb.headers = headers; + rb.credential = credential; + rb.signMethod = signMethod; + rb.unsignedPayload = unsignedPayload; + rb.language = language; + rb.requestClient = requestClient; + rb.now = now; + rb.nonce = nonce; + return rb; + } + + private RequestBuilder() { + } + + private static Object copyPayload(Object payload) { + if (payload instanceof byte[]) { + byte[] src = (byte[]) payload; + return Arrays.copyOf(src, src.length); + } + if (payload instanceof Map) { + return new LinkedHashMap((Map) payload); + } + return payload; + } + + private void addCallerHeaders(Headers.Builder hb) { + for (int i = 0, n = headers.size(); i < n; i++) { + hb.add(headers.name(i), headers.value(i)); + } + } + + private static void putHeaderIfNotEmpty(Headers.Builder hb, String name, String value) { + if (value != null && !value.isEmpty()) { + hb.set(name, value); + } + } + + private static void putIfNotNull(Map params, String key, String value) { + if (value != null) { + params.put(key, value); + } + } + + private static String hostHeaderOf(Request request) { + String h = request.header("Host"); + return (h != null && !h.isEmpty()) ? h : request.url().host(); + } + + private static Object payloadFromRequest(Request request, ContentType contentType) + throws IOException, TencentCloudSDKException { + if (HttpProfile.REQ_GET.equalsIgnoreCase(request.method())) { + return decodeQueryParams(request.url()); + } + RequestBody body = request.body(); + if (body == null) { + return new byte[0]; + } + Buffer buffer = new Buffer(); + body.writeTo(buffer); + byte[] bodyBytes = buffer.readByteArray(); + if (contentType == ContentType.FORM) { + return decodeFormParams(new String(bodyBytes, StandardCharsets.UTF_8)); + } + return bodyBytes; + } + + private static ContentType contentTypeFromHeader(String header, String method) { + if (HttpProfile.REQ_GET.equalsIgnoreCase(method)) { + return ContentType.FORM; + } + if (header != null && header.toLowerCase(Locale.ROOT).contains("application/octet-stream")) { + return ContentType.OCTET_STREAM; + } + if (header != null && header.toLowerCase(Locale.ROOT).contains("application/x-www-form-urlencoded")) { + return ContentType.FORM; + } + return ContentType.JSON; + } + + private static Headers callerHeadersFrom(Request request) { + Headers.Builder hb = new Headers.Builder(); + Headers h = request.headers(); + for (int i = 0, n = h.size(); i < n; i++) { + String name = h.name(i); + if (isSdkHeader(name)) { + continue; + } + hb.add(name, h.value(i)); + } + return hb.build(); + } + + private static boolean isSdkHeader(String name) { + return name.equalsIgnoreCase("Host") + || name.equalsIgnoreCase("Content-Type") + || name.equalsIgnoreCase("Authorization") + || name.equalsIgnoreCase("X-TC-Action") + || name.equalsIgnoreCase("X-TC-Version") + || name.equalsIgnoreCase("X-TC-Timestamp") + || name.equalsIgnoreCase("X-TC-RequestClient") + || name.equalsIgnoreCase("X-TC-Language") + || name.equalsIgnoreCase("X-TC-Region") + || name.equalsIgnoreCase("X-TC-Token") + || name.equalsIgnoreCase("X-TC-Content-SHA256"); + } + + private static String encodedPath(HttpUrl url) { + String p = url.encodedPath(); + return p == null || p.isEmpty() ? "/" : p; + } + + private static String utcDate(String timestamp) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.format(new Date(Long.valueOf(timestamp + "000"))); + } + + private static String encodedParams(Map params) throws TencentCloudSDKException { + TreeMap sorted = new TreeMap(params); + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : sorted.entrySet()) { + try { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(URLEncoder.encode(e.getKey(), "utf-8")) + .append("=") + .append(URLEncoder.encode(e.getValue(), "utf-8")); + } catch (UnsupportedEncodingException ex) { + throw new TencentCloudSDKException("UTF-8 not supported", ex); + } + } + return sb.toString(); + } + + private static Map decodeQueryParams(HttpUrl url) { + LinkedHashMap map = new LinkedHashMap(); + for (int i = 0, n = url.querySize(); i < n; i++) { + String value = url.queryParameterValue(i); + map.put(url.queryParameterName(i), value == null ? "" : value); + } + return map; + } + + private static Map decodeFormParams(String body) + throws TencentCloudSDKException { + LinkedHashMap map = new LinkedHashMap(); + if (body == null || body.isEmpty()) { + return map; + } + for (String pair : body.split("&")) { + int eq = pair.indexOf('='); + String k = eq < 0 ? pair : pair.substring(0, eq); + String v = eq < 0 ? "" : pair.substring(eq + 1); + try { + map.put(URLDecoder.decode(k, "utf-8"), URLDecoder.decode(v, "utf-8")); + } catch (UnsupportedEncodingException e) { + throw new TencentCloudSDKException("UTF-8 not supported", e); + } + } + return map; + } + + private static final class BodyAndQuery { + final byte[] body; + final String query; + + BodyAndQuery(byte[] body, String query) { + this.body = body == null ? new byte[0] : body; + this.query = query == null ? "" : query; + } + } +} diff --git a/src/main/java/com/tencentcloudapi/common/SSEResponseModel.java b/src/main/java/com/tencentcloudapi/common/SSEResponseModel.java index b495b02e1..965f7e4a9 100644 --- a/src/main/java/com/tencentcloudapi/common/SSEResponseModel.java +++ b/src/main/java/com/tencentcloudapi/common/SSEResponseModel.java @@ -29,7 +29,6 @@ public abstract class SSEResponseModel extends AbstractModel implements Iterable, Closeable { private Response response; - private CircuitBreaker.Token token; public abstract String getRequestId(); @@ -43,8 +42,15 @@ public boolean isStream() { return this.response != null; } + /** + * No-op since the region-failover CircuitBreaker was folded into + * {@link EndpointFailoverInterceptor}. Kept for binary/source compatibility + * with code compiled against earlier SDK versions. + * + * @deprecated Failover is now handled at the HTTP layer; this token has no effect. + */ + @Deprecated public void setToken(CircuitBreaker.Token token) { - this.token = token; } public static class SSE { diff --git a/src/main/java/com/tencentcloudapi/common/Sign.java b/src/main/java/com/tencentcloudapi/common/Sign.java index ebe1a2e6c..f622d9b4a 100644 --- a/src/main/java/com/tencentcloudapi/common/Sign.java +++ b/src/main/java/com/tencentcloudapi/common/Sign.java @@ -1,162 +1,162 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common; - -import com.tencentcloudapi.common.exception.TencentCloudSDKException; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.TreeMap; - -public class Sign { - - // UTF-8 character set - private static final Charset UTF8 = StandardCharsets.UTF_8; - - /** - * Signs the given string using the specified method and secret key. - * - * @param secretKey The secret key used to generate the signature. - * @param sigStr The string to sign. - * @param sigMethod The signing method (e.g., "HmacSHA256"). - * @return The generated signature in Base64 encoding. - * @throws TencentCloudSDKException If there is an error during signing. - */ - public static String sign(String secretKey, String sigStr, String sigMethod) throws TencentCloudSDKException { - String sig = null; - try { - // Create the Mac (Message Authentication Code) instance using the specified method - Mac mac = Mac.getInstance(sigMethod); - byte[] hash; - SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(UTF8), mac.getAlgorithm()); - - mac.init(secretKeySpec); - hash = mac.doFinal(sigStr.getBytes(UTF8)); - sig = DatatypeConverter.printBase64Binary(hash); - } catch (Exception e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - return sig; - } - - /** - * Constructs the plain text string for signing. - * - * @param requestParams The request parameters to include in the string. - * @param reqMethod The HTTP request method (GET, POST, etc.). - * @param host The host (API endpoint). - * @param path The request path. - * @return The plain text string used for signing. - */ - public static String makeSignPlainText(TreeMap requestParams, String reqMethod, String host, - String path) { - String retStr = ""; - retStr += reqMethod; - retStr += host; - retStr += path; - retStr += buildParamStr(requestParams, reqMethod); - return retStr; - } - - /** - * Builds the parameter string for signing, formatting the parameters in a query string format. - * - * @param requestParams The request parameters to include in the string. - * @param requestMethod The HTTP request method (GET, POST, etc.). - * @return The formatted parameter string. - */ - protected static String buildParamStr(TreeMap requestParams, String requestMethod) { - String retStr = ""; - // Iterate through the parameters and append them to the string - for (String key : requestParams.keySet()) { - String value = requestParams.get(key).toString(); - if (retStr.length() == 0) { - retStr += '?'; - } else { - retStr += '&'; - } - retStr += key.replace("_", ".") + '=' + value; - } - return retStr; - } - - /** - * Calculates the SHA-256 hash of the given string and returns it as a hexadecimal string. - * - * @param s The string to hash. - * @return The SHA-256 hash as a hexadecimal string. - * @throws TencentCloudSDKException If SHA-256 is not supported. - */ - public static String sha256Hex(String s) throws TencentCloudSDKException { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new TencentCloudSDKException("SHA-256 is not supported." + e.getMessage()); - } - byte[] d = md.digest(s.getBytes(UTF8)); - return DatatypeConverter.printHexBinary(d).toLowerCase(); - } - - /** - * Calculates the SHA-256 hash of the given byte array and returns it as a hexadecimal string. - * - * @param b The byte array to hash. - * @return The SHA-256 hash as a hexadecimal string. - * @throws TencentCloudSDKException If SHA-256 is not supported. - */ - public static String sha256Hex(byte[] b) throws TencentCloudSDKException { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new TencentCloudSDKException("SHA-256 is not supported." + e.getMessage()); - } - byte[] d = md.digest(b); - return DatatypeConverter.printHexBinary(d).toLowerCase(); - } - - /** - * Calculates the HMAC-SHA-256 signature of the given message using the specified key. - * - * @param key The key to use for HMAC. - * @param msg The message to sign. - * @return The HMAC-SHA-256 signature as a byte array. - * @throws TencentCloudSDKException If HMAC-SHA-256 is not supported. - */ - public static byte[] hmac256(byte[] key, String msg) throws TencentCloudSDKException { - Mac mac; - try { - mac = Mac.getInstance("HmacSHA256"); - } catch (NoSuchAlgorithmException e) { - throw new TencentCloudSDKException("HmacSHA256 is not supported." + e.getMessage()); - } - SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); - try { - mac.init(secretKeySpec); - } catch (InvalidKeyException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - return mac.doFinal(msg.getBytes(UTF8)); - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.TreeMap; + +public class Sign { + + // UTF-8 character set + private static final Charset UTF8 = StandardCharsets.UTF_8; + + /** + * Signs the given string using the specified method and secret key. + * + * @param secretKey The secret key used to generate the signature. + * @param sigStr The string to sign. + * @param sigMethod The signing method (e.g., "HmacSHA256"). + * @return The generated signature in Base64 encoding. + * @throws TencentCloudSDKException If there is an error during signing. + */ + public static String sign(String secretKey, String sigStr, String sigMethod) throws TencentCloudSDKException { + String sig = null; + try { + // Create the Mac (Message Authentication Code) instance using the specified method + Mac mac = Mac.getInstance(sigMethod); + byte[] hash; + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(UTF8), mac.getAlgorithm()); + + mac.init(secretKeySpec); + hash = mac.doFinal(sigStr.getBytes(UTF8)); + sig = DatatypeConverter.printBase64Binary(hash); + } catch (Exception e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + return sig; + } + + /** + * Constructs the plain text string for signing. + * + * @param requestParams The request parameters to include in the string. + * @param reqMethod The HTTP request method (GET, POST, etc.). + * @param host The host (API endpoint). + * @param path The request path. + * @return The plain text string used for signing. + */ + public static String makeSignPlainText(TreeMap requestParams, String reqMethod, String host, + String path) { + String retStr = ""; + retStr += reqMethod; + retStr += host; + retStr += path; + retStr += buildParamStr(requestParams, reqMethod); + return retStr; + } + + /** + * Builds the parameter string for signing, formatting the parameters in a query string format. + * + * @param requestParams The request parameters to include in the string. + * @param requestMethod The HTTP request method (GET, POST, etc.). + * @return The formatted parameter string. + */ + protected static String buildParamStr(TreeMap requestParams, String requestMethod) { + String retStr = ""; + // Iterate through the parameters and append them to the string + for (String key : requestParams.keySet()) { + String value = requestParams.get(key).toString(); + if (retStr.length() == 0) { + retStr += '?'; + } else { + retStr += '&'; + } + retStr += key.replace("_", ".") + '=' + value; + } + return retStr; + } + + /** + * Calculates the SHA-256 hash of the given string and returns it as a hexadecimal string. + * + * @param s The string to hash. + * @return The SHA-256 hash as a hexadecimal string. + * @throws TencentCloudSDKException If SHA-256 is not supported. + */ + public static String sha256Hex(String s) throws TencentCloudSDKException { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new TencentCloudSDKException("SHA-256 is not supported." + e.getMessage()); + } + byte[] d = md.digest(s.getBytes(UTF8)); + return DatatypeConverter.printHexBinary(d).toLowerCase(); + } + + /** + * Calculates the SHA-256 hash of the given byte array and returns it as a hexadecimal string. + * + * @param b The byte array to hash. + * @return The SHA-256 hash as a hexadecimal string. + * @throws TencentCloudSDKException If SHA-256 is not supported. + */ + public static String sha256Hex(byte[] b) throws TencentCloudSDKException { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new TencentCloudSDKException("SHA-256 is not supported." + e.getMessage()); + } + byte[] d = md.digest(b); + return DatatypeConverter.printHexBinary(d).toLowerCase(); + } + + /** + * Calculates the HMAC-SHA-256 signature of the given message using the specified key. + * + * @param key The key to use for HMAC. + * @param msg The message to sign. + * @return The HMAC-SHA-256 signature as a byte array. + * @throws TencentCloudSDKException If HMAC-SHA-256 is not supported. + */ + public static byte[] hmac256(byte[] key, String msg) throws TencentCloudSDKException { + Mac mac; + try { + mac = Mac.getInstance("HmacSHA256"); + } catch (NoSuchAlgorithmException e) { + throw new TencentCloudSDKException("HmacSHA256 is not supported." + e.getMessage()); + } + SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); + try { + mac.init(secretKeySpec); + } catch (InvalidKeyException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + return mac.doFinal(msg.getBytes(UTF8)); + } +} diff --git a/src/main/java/com/tencentcloudapi/common/TCLog.java b/src/main/java/com/tencentcloudapi/common/TCLog.java index 782f7c3d0..571c3af7c 100644 --- a/src/main/java/com/tencentcloudapi/common/TCLog.java +++ b/src/main/java/com/tencentcloudapi/common/TCLog.java @@ -94,7 +94,7 @@ public void debug(final String str, final Throwable t) { * @throws IOException If an I/O error occurs while processing the request or response. */ @Override - public Response intercept(Chain chain) throws IOException { + public Response intercept(Interceptor.Chain chain) throws IOException { // Get the request being sent Request request = chain.request(); String req = diff --git a/src/main/java/com/tencentcloudapi/common/exception/TencentCloudSDKException.java b/src/main/java/com/tencentcloudapi/common/exception/TencentCloudSDKException.java index 6f61362ca..4b73ae590 100644 --- a/src/main/java/com/tencentcloudapi/common/exception/TencentCloudSDKException.java +++ b/src/main/java/com/tencentcloudapi/common/exception/TencentCloudSDKException.java @@ -1,77 +1,77 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common.exception; - -public class TencentCloudSDKException extends Exception { - private static final long serialVersionUID = 1L; - - /** - * UUID of the request, it will be empty if request is not fulfilled. - */ - private String requestId; - - /** - * Error code, When API returns a failure, it must have an error code. - */ - private String errorCode; - - public TencentCloudSDKException(String message, Throwable cause) { - super(message, cause); - } - - public TencentCloudSDKException(String message) { - this(message, ""); - } - - public TencentCloudSDKException(String message, String requestId) { - this(message, requestId, ""); - } - - public TencentCloudSDKException(String message, String requestId, String errorCode) { - super(message); - this.requestId = requestId; - this.errorCode = errorCode; - } - - public String getRequestId() { - return requestId; - } - - /** - * Get error code - * - * @return A string represents error code - */ - public String getErrorCode() { - return errorCode; - } - - public String toString() { - String msg = "[TencentCloudSDKException]" - + "code: " - + this.getErrorCode() - + " message:" - + this.getMessage() - + " requestId:" - + this.getRequestId(); - if (getCause() != null) { - msg += " cause:" + getCause().toString(); - } - return msg; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common.exception; + +public class TencentCloudSDKException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * UUID of the request, it will be empty if request is not fulfilled. + */ + private String requestId; + + /** + * Error code, When API returns a failure, it must have an error code. + */ + private String errorCode; + + public TencentCloudSDKException(String message, Throwable cause) { + super(message, cause); + } + + public TencentCloudSDKException(String message) { + this(message, ""); + } + + public TencentCloudSDKException(String message, String requestId) { + this(message, requestId, ""); + } + + public TencentCloudSDKException(String message, String requestId, String errorCode) { + super(message); + this.requestId = requestId; + this.errorCode = errorCode; + } + + public String getRequestId() { + return requestId; + } + + /** + * Get error code + * + * @return A string represents error code + */ + public String getErrorCode() { + return errorCode; + } + + public String toString() { + String msg = "[TencentCloudSDKException]" + + "code: " + + this.getErrorCode() + + " message:" + + this.getMessage() + + " requestId:" + + this.getRequestId(); + if (getCause() != null) { + msg += " cause:" + getCause().toString(); + } + return msg; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/http/HttpConnection.java b/src/main/java/com/tencentcloudapi/common/http/HttpConnection.java index f837274e7..fd982f634 100644 --- a/src/main/java/com/tencentcloudapi/common/http/HttpConnection.java +++ b/src/main/java/com/tencentcloudapi/common/http/HttpConnection.java @@ -1,153 +1,158 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common.http; - -import com.tencentcloudapi.common.exception.TencentCloudSDKException; -import okhttp3.*; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; -import java.io.IOException; -import java.net.Proxy; -import java.util.concurrent.TimeUnit; - -public class HttpConnection { - - // https://stackoverflow.com/questions/31423154/performance-of-a-singleton-instance-okhttpclient - // https://github.com/square/okhttp/issues/3372 - // Creating dispatcher and connectionPool is expensive. - // Share them between OkHttpClients by singleton's Builder. - private static final OkHttpClient clientSingleton = new OkHttpClient(); - private OkHttpClient client; - - public HttpConnection(Integer connTimeout, Integer readTimeout, Integer writeTimeout) { - this.client = clientSingleton.newBuilder() - .connectTimeout(connTimeout, TimeUnit.SECONDS) - .readTimeout(readTimeout, TimeUnit.SECONDS) - .writeTimeout(writeTimeout, TimeUnit.SECONDS) - .build(); - } - - public void addInterceptors(Interceptor interceptor) { - this.client = this.client.newBuilder().addInterceptor(interceptor).build(); - } - - public void setProxy(Proxy proxy) { - this.client = this.client.newBuilder().proxy(proxy).build(); - } - - public void setProxyAuthenticator(Authenticator authenticator) { - this.client = this.client.newBuilder().proxyAuthenticator(authenticator).build(); - } - - @Deprecated - public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { - this.client = this.client.newBuilder().sslSocketFactory(sslSocketFactory).build(); - } - - public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) { - this.client = this.client.newBuilder().sslSocketFactory(sslSocketFactory, trustManager).build(); - } - - public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { - this.client = this.client.newBuilder().hostnameVerifier(hostnameVerifier).build(); - } - - public Response doRequest(Request request) throws IOException { - return this.client.newCall(request).execute(); - } - - public Response getRequest(String url) throws TencentCloudSDKException, IOException { - Request request = null; - try { - request = new Request.Builder().url(url).get().build(); - } catch (IllegalArgumentException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - - return this.doRequest(request); - } - - public Response getRequest(String url, Headers headers) throws TencentCloudSDKException, IOException { - Request request = null; - try { - request = new Request.Builder().url(url).headers(headers).get().build(); - } catch (IllegalArgumentException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - - return this.doRequest(request); - } - - public Response postRequest(String url, String body) throws TencentCloudSDKException, IOException { - MediaType contentType = MediaType.parse("application/x-www-form-urlencoded"); - Request request = null; - try { - request = new Request.Builder().url(url).post(RequestBody.create(contentType, body)).build(); - } catch (IllegalArgumentException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - - return this.doRequest(request); - } - - public Response postRequest(String url, String body, Headers headers) - throws TencentCloudSDKException, IOException { - MediaType contentType = MediaType.parse(headers.get("Content-Type")); - Request request = null; - try { - request = - new Request.Builder() - .url(url) - .post(RequestBody.create(contentType, body)) - .headers(headers) - .build(); - } catch (IllegalArgumentException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - - return this.doRequest(request); - } - - public Response postRequest(String url, byte[] body, Headers headers) - throws TencentCloudSDKException, IOException { - MediaType contentType = MediaType.parse(headers.get("Content-Type")); - Request request = null; - try { - request = - new Request.Builder() - .url(url) - .post(RequestBody.create(contentType, body)) - .headers(headers) - .build(); - } catch (IllegalArgumentException e) { - throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); - } - - return this.doRequest(request); - } - - public void setHttpClient(Object httpClient) { - client = (OkHttpClient) httpClient; - } - - public Object getHttpClient() { - return client; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common.http; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import okhttp3.*; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.Proxy; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class HttpConnection { + + // https://stackoverflow.com/questions/31423154/performance-of-a-singleton-instance-okhttpclient + // https://github.com/square/okhttp/issues/3372 + // Creating dispatcher and connectionPool is expensive. + // Share them between OkHttpClients by singleton's Builder. + private static final OkHttpClient clientSingleton = new OkHttpClient(); + private OkHttpClient client; + + public HttpConnection(Integer connTimeout, Integer readTimeout, Integer writeTimeout) { + this.client = clientSingleton.newBuilder() + .connectTimeout(connTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS) + .build(); + } + + public void addInterceptors(Interceptor interceptor) { + this.client = this.client.newBuilder().addInterceptor(interceptor).build(); + } + + public List getInterceptors() { + return this.client.interceptors(); + } + + public void setProxy(Proxy proxy) { + this.client = this.client.newBuilder().proxy(proxy).build(); + } + + public void setProxyAuthenticator(Authenticator authenticator) { + this.client = this.client.newBuilder().proxyAuthenticator(authenticator).build(); + } + + @Deprecated + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + this.client = this.client.newBuilder().sslSocketFactory(sslSocketFactory).build(); + } + + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) { + this.client = this.client.newBuilder().sslSocketFactory(sslSocketFactory, trustManager).build(); + } + + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.client = this.client.newBuilder().hostnameVerifier(hostnameVerifier).build(); + } + + public Response doRequest(Request request) throws IOException { + return this.client.newCall(request).execute(); + } + + public Response getRequest(String url) throws TencentCloudSDKException, IOException { + Request request = null; + try { + request = new Request.Builder().url(url).get().build(); + } catch (IllegalArgumentException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + + return this.doRequest(request); + } + + public Response getRequest(String url, Headers headers) throws TencentCloudSDKException, IOException { + Request request = null; + try { + request = new Request.Builder().url(url).headers(headers).get().build(); + } catch (IllegalArgumentException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + + return this.doRequest(request); + } + + public Response postRequest(String url, String body) throws TencentCloudSDKException, IOException { + MediaType contentType = MediaType.parse("application/x-www-form-urlencoded"); + Request request = null; + try { + request = new Request.Builder().url(url).post(RequestBody.create(contentType, body)).build(); + } catch (IllegalArgumentException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + + return this.doRequest(request); + } + + public Response postRequest(String url, String body, Headers headers) + throws TencentCloudSDKException, IOException { + MediaType contentType = MediaType.parse(headers.get("Content-Type")); + Request request = null; + try { + request = + new Request.Builder() + .url(url) + .post(RequestBody.create(contentType, body)) + .headers(headers) + .build(); + } catch (IllegalArgumentException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + + return this.doRequest(request); + } + + public Response postRequest(String url, byte[] body, Headers headers) + throws TencentCloudSDKException, IOException { + MediaType contentType = MediaType.parse(headers.get("Content-Type")); + Request request = null; + try { + request = + new Request.Builder() + .url(url) + .post(RequestBody.create(contentType, body)) + .headers(headers) + .build(); + } catch (IllegalArgumentException e) { + throw new TencentCloudSDKException(e.getClass().getName() + "-" + e.getMessage()); + } + + return this.doRequest(request); + } + + public void setHttpClient(Object httpClient) { + client = (OkHttpClient) httpClient; + } + + public Object getHttpClient() { + return client; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/profile/ClientProfile.java b/src/main/java/com/tencentcloudapi/common/profile/ClientProfile.java index c26389200..b1c71dc3b 100644 --- a/src/main/java/com/tencentcloudapi/common/profile/ClientProfile.java +++ b/src/main/java/com/tencentcloudapi/common/profile/ClientProfile.java @@ -1,214 +1,243 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common.profile; - -/** - * ClientProfile represents the profile configuration for a client - * in terms of signature methods, HTTP profile, language settings, - * and other configurations for interacting with Tencent Cloud APIs. - */ -public class ClientProfile { - - /** - * Constant for signature process using HmacSHA1 (version 1). - * This is an older signature method for API requests. - */ - public static final String SIGN_SHA1 = "HmacSHA1"; - - /** - * Constant for signature process using HmacSHA256 (version 1). - * This is a more secure signature method for API requests. - */ - public static final String SIGN_SHA256 = "HmacSHA256"; - - /** - * Constant for signature process using TC3-HMAC-SHA256 (version 3). - * This is the latest and most secure signature method for API requests. - */ - public static final String SIGN_TC3_256 = "TC3-HMAC-SHA256"; - - // HTTP profile associated with the client. - private HttpProfile httpProfile; - - // The method used for signing requests (HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). - private String signMethod; - - /** - * Flag indicating whether the payload (request body) is involved in the signing process. - * If true, the payload will be ignored during signing. - * Default value is false. - */ - private boolean unsignedPayload; - - /** - * Language setting for API responses. - * Valid options: zh-CN (Simplified Chinese), en-US (English). - */ - private Language language; - - // Flag indicating whether to enable debugging (logs for request/response details). - private boolean debug; - - // Backup endpoint for API requests, useful in case the primary endpoint fails. - private String backupEndpoint; - - /** - * Constructor to initialize ClientProfile with a specific signing method and HTTP profile. - * If the signing method is null or empty, it defaults to "TC3-HMAC-SHA256". - * - * @param signMethod The method used for signing the request (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). - * @param httpProfile The HTTP profile containing endpoint and connection settings. - */ - public ClientProfile(String signMethod, HttpProfile httpProfile) { - if (signMethod == null || signMethod.isEmpty()) { - signMethod = SIGN_TC3_256; - } - this.signMethod = signMethod; - this.httpProfile = httpProfile; - this.unsignedPayload = false; - this.language = null; - this.setDebug(false); - } - - /** - * Constructor to initialize ClientProfile with a specific signing method. - * Initializes the HTTP profile to a default new instance. - * - * @param signMethod The method used for signing the request (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). - */ - public ClientProfile(String signMethod) { - this(signMethod, new HttpProfile()); - } - - /** - * Default constructor which initializes the ClientProfile with the default - * signing method "TC3-HMAC-SHA256" and a default HTTP profile. - */ - public ClientProfile() { - this(ClientProfile.SIGN_TC3_256, new HttpProfile()); - } - - /** - * Getter for the signature method used in API requests. - * - * @return The signature method (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). - */ - public String getSignMethod() { - return this.signMethod; - } - - /** - * Setter for the signature method used in API requests. - * - * @param signMethod The signature method (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). - */ - public void setSignMethod(String signMethod) { - this.signMethod = signMethod; - } - - /** - * Getter for the HTTP profile associated with the client. - * - * @return The HTTP profile containing endpoint and connection settings. - */ - public HttpProfile getHttpProfile() { - return this.httpProfile; - } - - /** - * Setter for the HTTP profile associated with the client. - * - * @param httpProfile The new HTTP profile to be set. - */ - public void setHttpProfile(HttpProfile httpProfile) { - this.httpProfile = httpProfile; - } - - /** - * Getter for the flag indicating whether the payload is ignored during signing. - * - * @return true if the payload is ignored during signing, false otherwise. - */ - public boolean isUnsignedPayload() { - return this.unsignedPayload; - } - - /** - * Setter for the flag indicating whether the payload should be ignored during signing. - * This is only relevant for POST requests. - * - * @param flag Set to true if the payload should be ignored, false otherwise. - */ - public void setUnsignedPayload(boolean flag) { - this.unsignedPayload = flag; - } - - /** - * Getter for the language setting of the client. - * - * @return The current language setting (e.g., zh-CN, en-US). - */ - public Language getLanguage() { - return this.language; - } - - /** - * Setter for the language setting of the client. - * - * @param lang The language to set (e.g., zh-CN, en-US). - */ - public void setLanguage(Language lang) { - this.language = lang; - } - - /** - * Getter for the debug flag, which indicates whether debugging is enabled. - * - * @return true if debugging is enabled, false otherwise. - */ - public boolean isDebug() { - return debug; - } - - /** - * Setter for the debug flag, enabling or disabling debugging. - * - * @param debug Set to true to enable debugging, false to disable. - */ - public void setDebug(boolean debug) { - this.debug = debug; - } - - /** - * Getter for the backup endpoint, used when the primary endpoint is unavailable. - * - * @return The backup endpoint URL. - */ - public String getBackupEndpoint() { - return backupEndpoint; - } - - /** - * Setter for the backup endpoint. - * - * @param backupEndpoint The backup endpoint URL to be set. - */ - public void setBackupEndpoint(String backupEndpoint) { - this.backupEndpoint = backupEndpoint; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common.profile; + +/** + * ClientProfile represents the profile configuration for a client + * in terms of signature methods, HTTP profile, language settings, + * and other configurations for interacting with Tencent Cloud APIs. + */ +public class ClientProfile { + + /** + * Constant for signature process using HmacSHA1 (version 1). + * This is an older signature method for API requests. + */ + public static final String SIGN_SHA1 = "HmacSHA1"; + + /** + * Constant for signature process using HmacSHA256 (version 1). + * This is a more secure signature method for API requests. + */ + public static final String SIGN_SHA256 = "HmacSHA256"; + + /** + * Constant for signature process using TC3-HMAC-SHA256 (version 3). + * This is the latest and most secure signature method for API requests. + */ + public static final String SIGN_TC3_256 = "TC3-HMAC-SHA256"; + + // HTTP profile associated with the client. + private HttpProfile httpProfile; + + // The method used for signing requests (HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). + private String signMethod; + + /** + * Flag indicating whether the payload (request body) is involved in the signing process. + * If true, the payload will be ignored during signing. + * Default value is false. + */ + private boolean unsignedPayload; + + /** + * Language setting for API responses. + * Valid options: zh-CN (Simplified Chinese), en-US (English). + */ + private Language language; + + // Flag indicating whether to enable debugging (logs for request/response details). + private boolean debug; + + // Backup endpoint for API requests, useful in case the primary endpoint fails. + private String backupEndpoint; + + /** + * Whether to enable region-level domain failover. When true (default), the + * SDK automatically retries against backup TLDs (e.g. tencentcloudapi.com.cn / + * tencentcloudapi.cn) on DNS / TLS / network reachability failures of the + * primary domain. Custom apigw endpoints, region-pinned hosts, and any host + * the SDK does not recognise are passed through unchanged. + * + *

This field also controls the legacy single-fallback "backup endpoint" + * mode (see {@link #setBackupEndpoint}); both schemes are gated by the same + * switch. + */ + private boolean enableDomainFailover = true; + + /** + * Constructor to initialize ClientProfile with a specific signing method and HTTP profile. + * If the signing method is null or empty, it defaults to "TC3-HMAC-SHA256". + * + * @param signMethod The method used for signing the request (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). + * @param httpProfile The HTTP profile containing endpoint and connection settings. + */ + public ClientProfile(String signMethod, HttpProfile httpProfile) { + if (signMethod == null || signMethod.isEmpty()) { + signMethod = SIGN_TC3_256; + } + this.signMethod = signMethod; + this.httpProfile = httpProfile; + this.unsignedPayload = false; + this.language = null; + this.setDebug(false); + } + + /** + * Constructor to initialize ClientProfile with a specific signing method. + * Initializes the HTTP profile to a default new instance. + * + * @param signMethod The method used for signing the request (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). + */ + public ClientProfile(String signMethod) { + this(signMethod, new HttpProfile()); + } + + /** + * Default constructor which initializes the ClientProfile with the default + * signing method "TC3-HMAC-SHA256" and a default HTTP profile. + */ + public ClientProfile() { + this(ClientProfile.SIGN_TC3_256, new HttpProfile()); + } + + /** + * Getter for the signature method used in API requests. + * + * @return The signature method (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). + */ + public String getSignMethod() { + return this.signMethod; + } + + /** + * Setter for the signature method used in API requests. + * + * @param signMethod The signature method (e.g., HmacSHA1, HmacSHA256, TC3-HMAC-SHA256). + */ + public void setSignMethod(String signMethod) { + this.signMethod = signMethod; + } + + /** + * Getter for the HTTP profile associated with the client. + * + * @return The HTTP profile containing endpoint and connection settings. + */ + public HttpProfile getHttpProfile() { + return this.httpProfile; + } + + /** + * Setter for the HTTP profile associated with the client. + * + * @param httpProfile The new HTTP profile to be set. + */ + public void setHttpProfile(HttpProfile httpProfile) { + this.httpProfile = httpProfile; + } + + /** + * Getter for the flag indicating whether the payload is ignored during signing. + * + * @return true if the payload is ignored during signing, false otherwise. + */ + public boolean isUnsignedPayload() { + return this.unsignedPayload; + } + + /** + * Setter for the flag indicating whether the payload should be ignored during signing. + * This is only relevant for POST requests. + * + * @param flag Set to true if the payload should be ignored, false otherwise. + */ + public void setUnsignedPayload(boolean flag) { + this.unsignedPayload = flag; + } + + /** + * Getter for the language setting of the client. + * + * @return The current language setting (e.g., zh-CN, en-US). + */ + public Language getLanguage() { + return this.language; + } + + /** + * Setter for the language setting of the client. + * + * @param lang The language to set (e.g., zh-CN, en-US). + */ + public void setLanguage(Language lang) { + this.language = lang; + } + + /** + * Getter for the debug flag, which indicates whether debugging is enabled. + * + * @return true if debugging is enabled, false otherwise. + */ + public boolean isDebug() { + return debug; + } + + /** + * Setter for the debug flag, enabling or disabling debugging. + * + * @param debug Set to true to enable debugging, false to disable. + */ + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Getter for the backup endpoint, used when the primary endpoint is unavailable. + * + * @return The backup endpoint URL. + */ + public String getBackupEndpoint() { + return backupEndpoint; + } + + /** + * Setter for the backup endpoint. + * + * @param backupEndpoint The backup endpoint URL to be set. + */ + public void setBackupEndpoint(String backupEndpoint) { + this.backupEndpoint = backupEndpoint; + } + + /** + * @return true if region-level domain failover is enabled (default), false otherwise. + */ + public boolean isEnableDomainFailover() { + return this.enableDomainFailover; + } + + /** + * Enable or disable region-level domain failover. See {@link #enableDomainFailover}. + * + * @param enabled true to enable (default), false to disable. + */ + public void setEnableDomainFailover(boolean enabled) { + this.enableDomainFailover = enabled; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/profile/HttpProfile.java b/src/main/java/com/tencentcloudapi/common/profile/HttpProfile.java index f480adc3c..d7d7afe1a 100644 --- a/src/main/java/com/tencentcloudapi/common/profile/HttpProfile.java +++ b/src/main/java/com/tencentcloudapi/common/profile/HttpProfile.java @@ -1,416 +1,416 @@ -/* - * Copyright (c) 2018 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package com.tencentcloudapi.common.profile; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -/** - * HttpProfile defines the configuration settings for HTTP requests made by the Tencent Cloud client. - * It includes settings for the request method, endpoint, timeouts, proxy configurations, SSL settings, etc. - */ -public class HttpProfile { - - // Constants for request protocols and methods - public static final String REQ_HTTPS = "https://"; - public static final String REQ_HTTP = "http://"; - public static final String REQ_POST = "POST"; - public static final String REQ_GET = "GET"; - - /** - * Time unit constant: 1 minute equals 60 seconds. - */ - public static final int TM_MINUTE = 60; - - // HTTP method for the request (GET/POST) - private String reqMethod; - - /** - * The endpoint for the API request (e.g., "cvm.tencentcloudapi.com"). - */ - private String endpoint; - - /** - * The root domain of the API (e.g., "tencentcloudapi.com"). - */ - private String rootDomain; - - /** - * The protocol used for the request. Currently, only HTTPS is valid. - */ - private String protocol; - - /** - * Read timeout in seconds. Specifies the time to wait for a server response. - */ - private int readTimeout; - - /** - * Write timeout in seconds. Specifies the time to wait for data to be sent to the server. - */ - private int writeTimeout; - - /** - * Connect timeout in seconds. Specifies the time to wait for a connection to be established. - */ - private int connTimeout; - - /** - * HTTP proxy host, if a proxy is being used. - */ - private String proxyHost; - - /** - * HTTP proxy port, if a proxy is being used. - */ - private int proxyPort; - - /** - * HTTP proxy username, if authentication is required for the proxy. - */ - private String proxyUsername; - - /** - * HTTP proxy password, if authentication is required for the proxy. - */ - private String proxyPassword; - - // SSL-related fields for secure communication - private SSLSocketFactory sslSocketFactory; - private X509TrustManager trustManager; - private HostnameVerifier hostnameVerifier; - - /** - * The API Gateway endpoint, which can be different from the regular API endpoint. - */ - private String apigwEndpoint; - - /** - * For advanced users, an object for fine-grained control over the HTTP client. - * If this is set, other configurations like timeouts and proxy settings will be ignored. - */ - private Object httpClient; - - /** - * Default constructor for HttpProfile. - * Initializes default values for the HTTP profile configuration. - */ - public HttpProfile() { - this.reqMethod = HttpProfile.REQ_POST; - this.endpoint = null; - this.rootDomain = "tencentcloudapi.com"; - this.protocol = HttpProfile.REQ_HTTPS; - this.readTimeout = 0; - this.writeTimeout = 0; - this.connTimeout = HttpProfile.TM_MINUTE; - this.apigwEndpoint = null; - } - - /** - * Get the HTTP request method (e.g., POST or GET). - * - * @return The request method. - */ - public String getReqMethod() { - return this.reqMethod; - } - - /** - * Set the HTTP request method (e.g., POST or GET). - * - * @param reqMethod The HTTP method to set. - */ - public void setReqMethod(String reqMethod) { - this.reqMethod = reqMethod; - } - - /** - * Get the endpoint to which the request is sent. - * - * @return The endpoint (e.g., "cvm.tencentcloudapi.com"). - */ - public String getEndpoint() { - return this.endpoint; - } - - /** - * Set the endpoint for the API request. - * - *

Endpoint is the domain where the request is sent, e.g., [productName].tencentcloudapi.com. - * If you need to request a specific region (e.g., Guangzhou), set it to [productName].ap-guangzhou - * .tencentcloudapi.com. - * - * @param endpoint The endpoint URL. - */ - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - - /** - * Get the read timeout value in seconds. - * - * @return The read timeout in seconds. - */ - public int getReadTimeout() { - return this.readTimeout; - } - - /** - * Set the read timeout value in seconds. This specifies how long to wait for a response from the server. - * - * @param readTimeout The read timeout in seconds. - */ - public void setReadTimeout(int readTimeout) { - this.readTimeout = readTimeout; - } - - /** - * Get the write timeout value in seconds. - * - * @return The write timeout in seconds. - */ - public int getWriteTimeout() { - return this.writeTimeout; - } - - /** - * Set the write timeout value in seconds. This specifies how long to wait for the server to accept the data. - * - * @param writeTimeout The write timeout in seconds. - */ - public void setWriteTimeout(int writeTimeout) { - this.writeTimeout = writeTimeout; - } - - /** - * Get the connect timeout value in seconds. - * - * @return The connection timeout in seconds. - */ - public int getConnTimeout() { - return this.connTimeout; - } - - /** - * Set the connect timeout value in seconds. This specifies how long to wait for a connection to be established. - * - * @param connTimeout The connect timeout in seconds. - */ - public void setConnTimeout(int connTimeout) { - this.connTimeout = connTimeout; - } - - /** - * Get the protocol used for the request (HTTP or HTTPS). - * - * @return The protocol (e.g., "https://"). - */ - public String getProtocol() { - return this.protocol; - } - - /** - * Set the protocol for the request. Currently, only HTTPS is supported. - * - * @param protocol The protocol to use (e.g., "https://"). - */ - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - /** - * Get the proxy host if a proxy is being used. - * - * @return The proxy host. - */ - public String getProxyHost() { - return proxyHost; - } - - /** - * Set the proxy host for HTTP requests. - * - * @param proxyHost The proxy host to set. - */ - public void setProxyHost(String proxyHost) { - this.proxyHost = proxyHost; - } - - /** - * Get the proxy port if a proxy is being used. - * - * @return The proxy port. - */ - public int getProxyPort() { - return proxyPort; - } - - /** - * Set the proxy port for HTTP requests. - * - * @param proxyPort The proxy port to set. - */ - public void setProxyPort(int proxyPort) { - this.proxyPort = proxyPort; - } - - /** - * Get the proxy username for authentication. - * - * @return The proxy username. - */ - public String getProxyUsername() { - return proxyUsername; - } - - /** - * Set the proxy username for authentication. - * - * @param proxyUsername The proxy username to set. - */ - public void setProxyUsername(String proxyUsername) { - this.proxyUsername = proxyUsername; - } - - /** - * Get the proxy password for authentication. - * - * @return The proxy password. - */ - public String getProxyPassword() { - return proxyPassword; - } - - /** - * Set the proxy password for authentication. - * - * @param proxyPassword The proxy password to set. - */ - public void setProxyPassword(String proxyPassword) { - this.proxyPassword = proxyPassword; - } - - /** - * Get the root domain (e.g., "tencentcloudapi.com"). - * - * @return The root domain. - */ - public String getRootDomain() { - return rootDomain; - } - - /** - * Set the root domain of the API (e.g., "tencentcloudapi.com"). - * - * @param rootDomain The root domain to set. - */ - public void setRootDomain(String rootDomain) { - this.rootDomain = rootDomain; - } - - /** - * Get the SSLSocketFactory for SSL connections. - * - * @return The SSL socket factory. - */ - public SSLSocketFactory getSslSocketFactory() { - return sslSocketFactory; - } - - /** - * Set the SSLSocketFactory for SSL connections. - * - * @param sslSocketFactory The SSL socket factory to set. - */ - public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { - this.sslSocketFactory = sslSocketFactory; - } - - /** - * Get the trust manager for SSL connections. - * - * @return The X509 trust manager. - */ - public X509TrustManager getX509TrustManager() { - return trustManager; - } - - /** - * Set the trust manager for SSL connections. - * - * @param trustManager The X509 trust manager to set. - */ - public void setX509TrustManager(X509TrustManager trustManager) { - this.trustManager = trustManager; - } - - /** - * Get the API Gateway endpoint. - * - * @return The API Gateway endpoint. - */ - public String getApigwEndpoint() { - return apigwEndpoint; - } - - /** - * Set the API Gateway endpoint. - * - * @param apigwEndpoint The API Gateway endpoint to set. - */ - public void setApigwEndpoint(String apigwEndpoint) { - this.apigwEndpoint = apigwEndpoint; - } - - /** - * Get the HostnameVerifier used for hostname verification. - * - * @return The hostname verifier. - */ - public HostnameVerifier getHostnameVerifier() { - return hostnameVerifier; - } - - /** - * Set the HostnameVerifier used for hostname verification. - * - * @param hostnameVerifier The hostname verifier to set. - */ - public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { - this.hostnameVerifier = hostnameVerifier; - } - - /** - * Get the custom HTTP client for advanced configuration. - * - * @return The custom HTTP client. - */ - public Object getHttpClient() { - return httpClient; - } - - /** - * Set a custom HTTP client for advanced configuration. - * - * @param client The custom HTTP client to set. - */ - public void setHttpClient(Object client) { - httpClient = client; - } -} +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.tencentcloudapi.common.profile; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +/** + * HttpProfile defines the configuration settings for HTTP requests made by the Tencent Cloud client. + * It includes settings for the request method, endpoint, timeouts, proxy configurations, SSL settings, etc. + */ +public class HttpProfile { + + // Constants for request protocols and methods + public static final String REQ_HTTPS = "https://"; + public static final String REQ_HTTP = "http://"; + public static final String REQ_POST = "POST"; + public static final String REQ_GET = "GET"; + + /** + * Time unit constant: 1 minute equals 60 seconds. + */ + public static final int TM_MINUTE = 60; + + // HTTP method for the request (GET/POST) + private String reqMethod; + + /** + * The endpoint for the API request (e.g., "cvm.tencentcloudapi.com"). + */ + private String endpoint; + + /** + * The root domain of the API (e.g., "tencentcloudapi.com"). + */ + private String rootDomain; + + /** + * The protocol used for the request. Currently, only HTTPS is valid. + */ + private String protocol; + + /** + * Read timeout in seconds. Specifies the time to wait for a server response. + */ + private int readTimeout; + + /** + * Write timeout in seconds. Specifies the time to wait for data to be sent to the server. + */ + private int writeTimeout; + + /** + * Connect timeout in seconds. Specifies the time to wait for a connection to be established. + */ + private int connTimeout; + + /** + * HTTP proxy host, if a proxy is being used. + */ + private String proxyHost; + + /** + * HTTP proxy port, if a proxy is being used. + */ + private int proxyPort; + + /** + * HTTP proxy username, if authentication is required for the proxy. + */ + private String proxyUsername; + + /** + * HTTP proxy password, if authentication is required for the proxy. + */ + private String proxyPassword; + + // SSL-related fields for secure communication + private SSLSocketFactory sslSocketFactory; + private X509TrustManager trustManager; + private HostnameVerifier hostnameVerifier; + + /** + * The API Gateway endpoint, which can be different from the regular API endpoint. + */ + private String apigwEndpoint; + + /** + * For advanced users, an object for fine-grained control over the HTTP client. + * If this is set, other configurations like timeouts and proxy settings will be ignored. + */ + private Object httpClient; + + /** + * Default constructor for HttpProfile. + * Initializes default values for the HTTP profile configuration. + */ + public HttpProfile() { + this.reqMethod = HttpProfile.REQ_POST; + this.endpoint = null; + this.rootDomain = "tencentcloudapi.com"; + this.protocol = HttpProfile.REQ_HTTPS; + this.readTimeout = 0; + this.writeTimeout = 0; + this.connTimeout = HttpProfile.TM_MINUTE; + this.apigwEndpoint = null; + } + + /** + * Get the HTTP request method (e.g., POST or GET). + * + * @return The request method. + */ + public String getReqMethod() { + return this.reqMethod; + } + + /** + * Set the HTTP request method (e.g., POST or GET). + * + * @param reqMethod The HTTP method to set. + */ + public void setReqMethod(String reqMethod) { + this.reqMethod = reqMethod; + } + + /** + * Get the endpoint to which the request is sent. + * + * @return The endpoint (e.g., "cvm.tencentcloudapi.com"). + */ + public String getEndpoint() { + return this.endpoint; + } + + /** + * Set the endpoint for the API request. + * + *

Endpoint is the domain where the request is sent, e.g., [productName].tencentcloudapi.com. + * If you need to request a specific region (e.g., Guangzhou), set it to [productName].ap-guangzhou + * .tencentcloudapi.com. + * + * @param endpoint The endpoint URL. + */ + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + /** + * Get the read timeout value in seconds. + * + * @return The read timeout in seconds. + */ + public int getReadTimeout() { + return this.readTimeout; + } + + /** + * Set the read timeout value in seconds. This specifies how long to wait for a response from the server. + * + * @param readTimeout The read timeout in seconds. + */ + public void setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + } + + /** + * Get the write timeout value in seconds. + * + * @return The write timeout in seconds. + */ + public int getWriteTimeout() { + return this.writeTimeout; + } + + /** + * Set the write timeout value in seconds. This specifies how long to wait for the server to accept the data. + * + * @param writeTimeout The write timeout in seconds. + */ + public void setWriteTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + } + + /** + * Get the connect timeout value in seconds. + * + * @return The connection timeout in seconds. + */ + public int getConnTimeout() { + return this.connTimeout; + } + + /** + * Set the connect timeout value in seconds. This specifies how long to wait for a connection to be established. + * + * @param connTimeout The connect timeout in seconds. + */ + public void setConnTimeout(int connTimeout) { + this.connTimeout = connTimeout; + } + + /** + * Get the protocol used for the request (HTTP or HTTPS). + * + * @return The protocol (e.g., "https://"). + */ + public String getProtocol() { + return this.protocol; + } + + /** + * Set the protocol for the request. Currently, only HTTPS is supported. + * + * @param protocol The protocol to use (e.g., "https://"). + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + /** + * Get the proxy host if a proxy is being used. + * + * @return The proxy host. + */ + public String getProxyHost() { + return proxyHost; + } + + /** + * Set the proxy host for HTTP requests. + * + * @param proxyHost The proxy host to set. + */ + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + /** + * Get the proxy port if a proxy is being used. + * + * @return The proxy port. + */ + public int getProxyPort() { + return proxyPort; + } + + /** + * Set the proxy port for HTTP requests. + * + * @param proxyPort The proxy port to set. + */ + public void setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + } + + /** + * Get the proxy username for authentication. + * + * @return The proxy username. + */ + public String getProxyUsername() { + return proxyUsername; + } + + /** + * Set the proxy username for authentication. + * + * @param proxyUsername The proxy username to set. + */ + public void setProxyUsername(String proxyUsername) { + this.proxyUsername = proxyUsername; + } + + /** + * Get the proxy password for authentication. + * + * @return The proxy password. + */ + public String getProxyPassword() { + return proxyPassword; + } + + /** + * Set the proxy password for authentication. + * + * @param proxyPassword The proxy password to set. + */ + public void setProxyPassword(String proxyPassword) { + this.proxyPassword = proxyPassword; + } + + /** + * Get the root domain (e.g., "tencentcloudapi.com"). + * + * @return The root domain. + */ + public String getRootDomain() { + return rootDomain; + } + + /** + * Set the root domain of the API (e.g., "tencentcloudapi.com"). + * + * @param rootDomain The root domain to set. + */ + public void setRootDomain(String rootDomain) { + this.rootDomain = rootDomain; + } + + /** + * Get the SSLSocketFactory for SSL connections. + * + * @return The SSL socket factory. + */ + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + /** + * Set the SSLSocketFactory for SSL connections. + * + * @param sslSocketFactory The SSL socket factory to set. + */ + public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + /** + * Get the trust manager for SSL connections. + * + * @return The X509 trust manager. + */ + public X509TrustManager getX509TrustManager() { + return trustManager; + } + + /** + * Set the trust manager for SSL connections. + * + * @param trustManager The X509 trust manager to set. + */ + public void setX509TrustManager(X509TrustManager trustManager) { + this.trustManager = trustManager; + } + + /** + * Get the API Gateway endpoint. + * + * @return The API Gateway endpoint. + */ + public String getApigwEndpoint() { + return apigwEndpoint; + } + + /** + * Set the API Gateway endpoint. + * + * @param apigwEndpoint The API Gateway endpoint to set. + */ + public void setApigwEndpoint(String apigwEndpoint) { + this.apigwEndpoint = apigwEndpoint; + } + + /** + * Get the HostnameVerifier used for hostname verification. + * + * @return The hostname verifier. + */ + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Set the HostnameVerifier used for hostname verification. + * + * @param hostnameVerifier The hostname verifier to set. + */ + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + } + + /** + * Get the custom HTTP client for advanced configuration. + * + * @return The custom HTTP client. + */ + public Object getHttpClient() { + return httpClient; + } + + /** + * Set a custom HTTP client for advanced configuration. + * + * @param client The custom HTTP client to set. + */ + public void setHttpClient(Object client) { + httpClient = client; + } +} diff --git a/src/main/java/com/tencentcloudapi/common/provider/DefaultCredentialsProvider.java b/src/main/java/com/tencentcloudapi/common/provider/DefaultCredentialsProvider.java index 2d7c77d66..9d57a14b7 100644 --- a/src/main/java/com/tencentcloudapi/common/provider/DefaultCredentialsProvider.java +++ b/src/main/java/com/tencentcloudapi/common/provider/DefaultCredentialsProvider.java @@ -24,7 +24,13 @@ public Credential getCredentials() throws TencentCloudSDKException { return cred; } - cred = new OIDCRoleArnProvider().getCredentials(); - return cred; + try { + cred = new OIDCRoleArnProvider().getCredentials(); + return cred; + } catch (TencentCloudSDKException e) { + // OIDC not configured or unavailable; fall through + } + + throw new TencentCloudSDKException("No valid credential"); } } diff --git a/src/main/java/com/tencentcloudapi/common/provider/ProfileCredentialsProvider.java b/src/main/java/com/tencentcloudapi/common/provider/ProfileCredentialsProvider.java index a54c0cdea..ba272316e 100644 --- a/src/main/java/com/tencentcloudapi/common/provider/ProfileCredentialsProvider.java +++ b/src/main/java/com/tencentcloudapi/common/provider/ProfileCredentialsProvider.java @@ -2,17 +2,18 @@ import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; -import org.ini4j.Wini; +import org.apache.commons.configuration2.INIConfiguration; +import org.apache.commons.configuration2.ex.ConfigurationException; -import java.io.File; +import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; public class ProfileCredentialsProvider implements CredentialsProvider { - private static Wini ini; + private static INIConfiguration ini; - private static Wini getIni() throws TencentCloudSDKException { + private static INIConfiguration getIni() throws TencentCloudSDKException { String fileName; if (Files.exists(Paths.get(System.getProperty("user.home") + "\\.tencentcloud\\credentials"))) { fileName = System.getProperty("user.home") + "\\.tencentcloud\\credentials"; @@ -23,19 +24,20 @@ private static Wini getIni() throws TencentCloudSDKException { } else { throw new TencentCloudSDKException("Not found file"); } - try { - ini = new Wini(new File(fileName)); - } catch (IOException e) { - throw new TencentCloudSDKException("IOException"); + try (FileReader reader = new FileReader(fileName)) { + ini = new INIConfiguration(); + ini.read(reader); + } catch (IOException | ConfigurationException e) { + throw new TencentCloudSDKException("IOException or ConfigurationException"); } return ini; } @Override public Credential getCredentials() throws TencentCloudSDKException { - Wini ini = getIni(); - String secretId = ini.get("default", "secret_id"); - String secretKey = ini.get("default", "secret_key"); + INIConfiguration ini = getIni(); + String secretId = ini.getString("default.secret_id"); + String secretKey = ini.getString("default.secret_key"); if (secretId == null || secretKey == null) { throw new TencentCloudSDKException("Not found secretId or secretKey"); } diff --git a/src/test/java/com/tencentcloudapi/common/AbstractClientProxyAuthTest.java b/src/test/java/com/tencentcloudapi/common/AbstractClientProxyAuthTest.java new file mode 100644 index 000000000..81b1a1791 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/AbstractClientProxyAuthTest.java @@ -0,0 +1,120 @@ +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.http.HttpConnection; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +/** + * Unit tests for the proxy authentication setup in {@link AbstractClient}. + * + *

Regression coverage for the okhttp 3.x -> 4.x upgrade: okhttp 4's + * {@code Credentials.basic(username, password)} rejects a null password with an NPE, + * so {@code AbstractClient.trySetProxy} now falls back to an empty password when the + * profile only configures a proxy username. + */ +public class AbstractClientProxyAuthTest { + + /** + * Minimal concrete subclass: AbstractClient declares no abstract methods, so an empty + * subclass is enough to instantiate it. The constructor performs no network I/O. + */ + private static final class TestClient extends AbstractClient { + TestClient(ClientProfile profile) { + super("cvm.tencentcloudapi.com", "2017-03-12", + new Credential("secret-id", "secret-key"), "ap-guangzhou", profile); + } + } + + private static ClientProfile proxyProfile(String username, String password) { + HttpProfile httpProfile = new HttpProfile(); + httpProfile.setProxyHost("127.0.0.1"); + httpProfile.setProxyPort(8080); + if (username != null) { + httpProfile.setProxyUsername(username); + } + if (password != null) { + httpProfile.setProxyPassword(password); + } + ClientProfile profile = new ClientProfile(); + profile.setHttpProfile(httpProfile); + return profile; + } + + private static OkHttpClient extractOkHttpClient(AbstractClient client) throws Exception { + Field connField = AbstractClient.class.getDeclaredField("httpConnection"); + connField.setAccessible(true); + HttpConnection conn = (HttpConnection) connField.get(client); + Field clientField = HttpConnection.class.getDeclaredField("client"); + clientField.setAccessible(true); + return (OkHttpClient) clientField.get(conn); + } + + private static Response fakeProxyChallenge() { + Request request = new Request.Builder() + .url("https://cvm.tencentcloudapi.com/") + .build(); + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(407) + .message("Proxy Authentication Required") + .build(); + } + + /** + * The core regression: with only a proxy username configured (password is null), the + * authenticator must not throw on okhttp 4, and must send an empty password. + * base64("user:") = "dXNlcjo=". + */ + @Test + public void nullProxyPasswordFallsBackToEmpty() throws Exception { + OkHttpClient ok = extractOkHttpClient(new TestClient(proxyProfile("user", null))); + Authenticator authenticator = ok.proxyAuthenticator(); + Request authenticated = authenticator.authenticate(null, fakeProxyChallenge()); + assertEquals("Basic dXNlcjo=", authenticated.header("Proxy-Authorization")); + } + + /** + * A configured password must be passed through unchanged. + * base64("user:pass") = "dXNlcjpwYXNz". + */ + @Test + public void configuredProxyPasswordIsUsed() throws Exception { + OkHttpClient ok = extractOkHttpClient(new TestClient(proxyProfile("user", "pass"))); + Authenticator authenticator = ok.proxyAuthenticator(); + Request authenticated = authenticator.authenticate(null, fakeProxyChallenge()); + assertEquals("Basic dXNlcjpwYXNz", authenticated.header("Proxy-Authorization")); + } + + /** + * Proxy host configured but no username: no authenticator should be installed + * (okhttp default is Authenticator.NONE). + */ + @Test + public void proxyWithoutUsernameKeepsDefaultAuthenticator() throws Exception { + OkHttpClient ok = extractOkHttpClient(new TestClient(proxyProfile(null, "pass"))); + assertSame(Authenticator.NONE, ok.proxyAuthenticator()); + } + + /** + * No proxy configured at all: neither proxy nor authenticator should be installed. + */ + @Test + public void noProxyKeepsDefaults() throws Exception { + OkHttpClient ok = extractOkHttpClient(new TestClient(new ClientProfile())); + assertSame(Authenticator.NONE, ok.proxyAuthenticator()); + assertNull(ok.proxy()); + } +} diff --git a/src/test/java/com/tencentcloudapi/common/CircuitBreakerUnitTest.java b/src/test/java/com/tencentcloudapi/common/CircuitBreakerUnitTest.java new file mode 100644 index 000000000..e00003453 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/CircuitBreakerUnitTest.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for {@link CircuitBreaker} state-machine branches not covered by + * the basic smoke tests in + * {@code integration/common/CircuitBreakerTest.java}. + * + *

Covers: failure-percentage threshold, consecutive-failure threshold, + * HalfOpen success path, stale-generation report discarding, window-interval + * counter reset, custom {@link CircuitBreaker.Setting} combinations, and + * {@link EndpointFailoverInterceptor#breakerFor(String)} inheriting the region + * breaker's settings. + */ +public class CircuitBreakerUnitTest { + + // ================================================================= + // readyToOpen: maxFailPercentage threshold + // ================================================================= + + /** + * {@code readyToOpen()} fires when {@code failures >= maxFailNum && failPre + * >= maxFailPercentage}. To isolate the percentage branch from the + * consecutive-failure branch (consecutiveFailures > 5) and from the pure + * count branch, use maxFailNum=6 so the count alone wouldn't fire at 5 + * failures but the conjunction does once both thresholds are met. + * + *

Sequence with maxFailNum=6, maxFailPercentage=0.75 (default): + * 3 failures + 1 success + 3 failures = failures=6, all=7, failPre=6/7≈0.857 + * ≥ 0.75 and failures=6 >= 6 → opens. The success keeps consecutiveFailures + * at 3 on the final failure, so the consecutive branch (consecutiveFailures > 5) + * does not fire first. + */ + @Test + public void testOpensOnFailurePercentageThreshold() { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.maxFailNum = 6; // count branch alone needs >= 6 + CircuitBreaker cb = new CircuitBreaker(setting); + CircuitBreaker.Token t; + + // 3 failures (consecutiveFailures=3, failures=3, all=3). + for (int i = 0; i < 3; i++) { + t = cb.allow(); + Assert.assertTrue("call " + (i + 1) + " should be allowed", t.allowed); + t.report(false); + } + + // 1 success resets consecutiveFailures but keeps failures=3, all=4. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(true); + + // 2 more failures: failures=5, all=6, failPre=5/6≈0.83 ≥ 0.75 but + // failures=5 < 6 → not open yet (count branch not satisfied). + for (int i = 0; i < 2; i++) { + t = cb.allow(); + Assert.assertTrue("failure " + (i + 1) + " should be allowed", t.allowed); + t.report(false); + } + + // 3rd failure after success: failures=6 >= 6 and failPre=6/7≈0.857 + // ≥ 0.75 → opens. consecutiveFailures=3 so the consecutive branch + // is not the trigger. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + + t = cb.allow(); + Assert.assertFalse( + "percentage branch should have opened the breaker", t.allowed); + } + + // ================================================================= + // readyToOpen: consecutiveFailures > 5 threshold + // ================================================================= + + /** + * {@code readyToOpen()}'s second condition: {@code consecutiveFailures > 5} + * opens the breaker regardless of failure count or percentage. With 5 + * consecutive failures {@code consecutiveFailures == 5} (not > 5), so the + * breaker stays closed; the 6th consecutive failure trips it. + */ + @Test + public void testOpensOnSixConsecutiveFailures() { + CircuitBreaker cb = new CircuitBreaker(); + CircuitBreaker.Token t; + + // 5 consecutive failures — fails the percentage branch check too + // (failures=5 >= 5, failPre=1.0 >= 0.75), so this actually opens at 5. + // To isolate the consecutiveFailures branch we must keep failPre low, + // but maxFailNum=5 dominates once reached. Use a custom setting where + // maxFailNum is high so only the consecutive branch can fire. + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.maxFailNum = 100; // disable the count branch + setting.maxFailPercentage = 1.0f; // disable the percentage branch + cb = new CircuitBreaker(setting); + + // 5 consecutive failures: consecutiveFailures=5, not > 5 → stays closed. + for (int i = 0; i < 5; i++) { + t = cb.allow(); + Assert.assertTrue("call " + (i + 1) + " should be allowed", t.allowed); + t.report(false); + } + + // Insert a success to prove consecutiveFailures is the trigger, not + // total failures: reset consecutiveFailures, then accumulate again. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(true); + + // 5 more consecutive failures — consecutiveFailures=5 again, still closed. + for (int i = 0; i < 5; i++) { + t = cb.allow(); + Assert.assertTrue("call " + (i + 1) + " after reset should be allowed", t.allowed); + t.report(false); + } + + // 6th consecutive failure (consecutiveFailures=6 > 5) → opens. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + + t = cb.allow(); + Assert.assertFalse("breaker should be open on 6th consecutive failure", t.allowed); + } + + // ================================================================= + // HalfOpen success → Closed + // ================================================================= + + /** + * {@code onSuccess(State.HalfOpen, ...)} transitions to Closed when + * {@code all - failures > maxRequests}. With default maxRequests=0, the + * first HalfOpen success (all=1, failures=0, 1-0>0) closes the breaker. + * This complements {@code integration} testFail5Recover2 which only + * covers the HalfOpen-failure → re-Open path. + */ + @Test + public void testHalfOpenSuccessClosesBreaker() throws InterruptedException { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.timeoutMs = 100; + CircuitBreaker cb = new CircuitBreaker(setting); + CircuitBreaker.Token t; + + // 5 failures → Open. + for (int i = 0; i < 5; i++) { + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + } + t = cb.allow(); + Assert.assertFalse(t.allowed); + + // Wait past Open timeout → HalfOpen. + Thread.sleep(setting.timeoutMs + 20); + + // First HalfOpen probe succeeds → Closed. + t = cb.allow(); + Assert.assertTrue("HalfOpen probe should be allowed", t.allowed); + t.report(true); + + // Now Closed: subsequent calls are allowed and a fresh failure does + // not immediately re-open (consecutiveFailures=1, not > 5; failures=1 + // < 5). + t = cb.allow(); + Assert.assertTrue("Closed after HalfOpen success", t.allowed); + t.report(false); + t = cb.allow(); + Assert.assertTrue("single failure in fresh Closed window should not re-open", t.allowed); + } + + // ================================================================= + // Stale-generation report is discarded + // ================================================================= + + /** + * {@code report()} returns early when {@code result.generation != + * beforeGeneration}. A token captured before a state-transition-induced + * generation bump must not affect the new generation's counters. + * + *

Sequence: 5 failures → Open (gen=1). Capture a token (gen=1) but do + * not report. Sleep past Open timeout → HalfOpen (gen=2). Reporting the + * stale gen=1 token with failure must NOT trip HalfOpen back to Open — + * the next allow must still be permitted (HalfOpen). + */ + @Test + public void testStaleGenerationReportIsDiscarded() throws InterruptedException { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.timeoutMs = 100; + CircuitBreaker cb = new CircuitBreaker(setting); + CircuitBreaker.Token t; + + for (int i = 0; i < 5; i++) { + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + } + // Open — capture a token but hold it without reporting. + CircuitBreaker.Token staleToken = cb.allow(); + Assert.assertFalse(staleToken.allowed); + + // Sleep into HalfOpen (new generation). + Thread.sleep(setting.timeoutMs + 20); + CircuitBreaker.Token halfOpenToken = cb.allow(); + Assert.assertTrue("should be in HalfOpen", halfOpenToken.allowed); + + // Report the stale token as a failure — must be discarded. + staleToken.report(false); + + // The HalfOpen probe should still be viable: reporting it as success + // should close the breaker. If the stale report had been applied, + // HalfOpen would have flipped back to Open and the success report + // would land on a new generation and be discarded, leaving the next + // allow() rejected. + halfOpenToken.report(true); + CircuitBreaker.Token after = cb.allow(); + Assert.assertTrue( + "stale failure report must not have flipped HalfOpen back to Open", + after.allowed); + } + + // ================================================================= + // windowIntervalMs resets Closed counters + // ================================================================= + + /** + * In Closed state, when {@code expiry < now} the breaker calls + * {@code toNewGeneration}, zeroing {@code failures} and {@code all} (but + * NOT {@code consecutiveFailures} — that field is only reset by a success + * or a state transition). Verify the window reset by showing that a fresh + * failure burst opens the breaker later than it would if the counters had + * carried over. + * + *

With maxFailNum=5, maxFailPercentage=0.75: 3 failures + 1 success + * (resets consecutiveFailures) + window expiry + 5 failures. With reset, + * failures climbs 1..5 and opens on the 5th (failures=5 >= 5, failPre=1.0 + * >= 0.75, consecutiveFailures=5, not > 5). Without reset, failures would + * be 3+2=5 on the 2nd post-window failure and open early. + */ + @Test + public void testWindowIntervalResetsClosedCounters() throws InterruptedException { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.windowIntervalMs = 200; + setting.timeoutMs = 200; + CircuitBreaker cb = new CircuitBreaker(setting); + CircuitBreaker.Token t; + + // 3 failures (failures=3, all=3, consecutiveFailures=3). + for (int i = 0; i < 3; i++) { + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + } + + // 1 success: consecutiveFailures=0, failures=3, all=4. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(true); + + // Wait past the Closed window so the next allow() starts a new + // generation with zeroed failures/all. + Thread.sleep(setting.windowIntervalMs + 100); + + // 4 post-window failures: with reset, failures=4 < 5 → still closed. + // (consecutiveFailures=4, not > 5.) Without reset, failures would be + // 3+4=7 and the breaker would have opened at the 2nd failure here. + for (int i = 0; i < 4; i++) { + t = cb.allow(); + Assert.assertTrue( + "failure " + (i + 1) + " after reset should be allowed", t.allowed); + t.report(false); + } + + // 5th post-window failure: failures=5 >= 5, failPre=5/5=1.0 >= 0.75 → opens. + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + + t = cb.allow(); + Assert.assertFalse( + "breaker should open once failures re-accumulates past threshold", + t.allowed); + } + + // ================================================================= + // Custom Setting combinations + // ================================================================= + + /** + * Exercises non-default {@link CircuitBreaker.Setting} values together: + * maxFailNum=3 lowers the count threshold; maxFailPercentage=0.5 lowers + * the percentage threshold; maxRequests=2 requires more than 2 + * HalfOpen successes (with no failures) before closing. + */ + @Test + public void testCustomSettingCombination() throws InterruptedException { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.maxFailNum = 3; + setting.maxFailPercentage = 0.5f; + setting.timeoutMs = 100; + setting.maxRequests = 2; + CircuitBreaker cb = new CircuitBreaker(setting); + CircuitBreaker.Token t; + + // 3 failures, failPre=1.0 >= 0.5 and failures=3 >= 3 → opens on 3rd. + for (int i = 0; i < 3; i++) { + t = cb.allow(); + Assert.assertTrue(t.allowed); + t.report(false); + } + t = cb.allow(); + Assert.assertFalse("custom maxFailNum=3 should open after 3 failures", t.allowed); + + // Sleep into HalfOpen. + Thread.sleep(setting.timeoutMs + 20); + + // maxRequests=2: need all - failures > 2, i.e. at least 3 successes. + // 2 successes → all=2, failures=0, 2-0=2 not > 2 → stays HalfOpen. + for (int i = 0; i < 2; i++) { + t = cb.allow(); + Assert.assertTrue("HalfOpen probe " + (i + 1) + " should be allowed", t.allowed); + t.report(true); + } + // 3rd success → all=3, 3-0=3 > 2 → Closed. + t = cb.allow(); + Assert.assertTrue("HalfOpen probe 3 should be allowed", t.allowed); + t.report(true); + + // Closed again — fresh allow must succeed and a single failure must + // not re-open (failures=1 < 3). + t = cb.allow(); + Assert.assertTrue("should be Closed after 3 HalfOpen successes", t.allowed); + t.report(false); + t = cb.allow(); + Assert.assertTrue("single failure should not re-open custom-threshold breaker", t.allowed); + } + + // ================================================================= + // EndpointFailoverInterceptor.newBreaker inherits regionBreaker setting + // ================================================================= + + /** + * {@link EndpointFailoverInterceptor#breakerFor(String)} calls + * {@code newBreaker()}, which — when a region breaker has been set — + * constructs the per-host breaker with {@code regionBreaker.getSetting()} + * instead of the default. Verify the inherited setting takes effect by + * checking the per-host breaker opens after the region breaker's custom + * maxFailNum, not the default 5. + */ + @Test + public void testNewBreakerInheritsRegionBreakerSetting() throws Exception { + // Build an interceptor via reflection: the constructor needs an + // AbstractClient, but newBreaker()/breakerFor()/setRegionBreaker() + // only read client.getClientProfile().getBackupEndpoint() in the + // constructor. Use a real CvmClient with a default profile. + com.tencentcloudapi.common.profile.ClientProfile profile = + new com.tencentcloudapi.common.profile.ClientProfile(); + com.tencentcloudapi.cvm.v20170312.CvmClient client = + new com.tencentcloudapi.cvm.v20170312.CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + + EndpointFailoverInterceptor interceptor = + new EndpointFailoverInterceptor(client); + + CircuitBreaker.Setting regionSetting = new CircuitBreaker.Setting(); + regionSetting.maxFailNum = 3; + regionSetting.maxFailPercentage = 1.0f; // isolate the count branch + CircuitBreaker regionBreaker = new CircuitBreaker(regionSetting); + interceptor.setRegionBreaker(regionBreaker); + + // Trigger creation of a per-host breaker. + String host = "cvm.tencentcloudapi.com"; + CircuitBreaker perHost = interceptor.breakerFor(host); + + // The new breaker should share the region breaker's Setting instance + // (newBreaker returns new CircuitBreaker(regionBreaker.getSetting())). + Assert.assertSame( + "per-host breaker should inherit regionBreaker's Setting instance", + regionSetting, perHost.getSetting()); + + // Behavioural check: opens after 3 failures (region's maxFailNum), + // not 5 (default). With maxFailPercentage=1.0f the percentage branch + // never fires, so only the count branch (failures >= 3) can open it. + CircuitBreaker.Token t; + for (int i = 0; i < 3; i++) { + t = perHost.allow(); + Assert.assertTrue("failure " + (i + 1) + " should be allowed", t.allowed); + t.report(false); + } + t = perHost.allow(); + Assert.assertFalse( + "inherited maxFailNum=3 should open after 3 failures", t.allowed); + } +} diff --git a/src/test/java/com/tencentcloudapi/common/CommonRequestTest.java b/src/test/java/com/tencentcloudapi/common/CommonRequestTest.java new file mode 100644 index 000000000..3743f8764 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/CommonRequestTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common; + +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link CommonRequest}. + * + *

{@link CommonRequest} wraps a JSON string into an {@link AbstractModel} + * by deserialising it into a {@code HashMap} and feeding each + * entry into {@code set(key, value)}. Its {@code toMap} then recursively + * flattens nested maps/lists into dot-prefixed string keys. + */ +public class CommonRequestTest { + + // ================================================================= + // Constructor / deserialisation + // ================================================================= + + /** + * The constructor deserialises the JSON and writes each top-level entry + * into {@code any()} (the parent's {@code customizedParams}). + */ + @Test + public void testConstructorPopulatesAnyFromJson() { + CommonRequest req = new CommonRequest("{\"Region\":\"ap-guangzhou\",\"Limit\":10}"); + HashMap any = req.any(); + assertEquals("ap-guangzhou", any.get("Region")); + // LONG_OR_DOUBLE strategy: integer → Long + assertTrue("Limit should be a Long under LONG_OR_DOUBLE strategy, got " + + (any.get("Limit") == null ? "null" : any.get("Limit").getClass().getName()), + any.get("Limit") instanceof Long); + assertEquals(10L, any.get("Limit")); + } + + /** + * LONG_OR_DOUBLE number strategy: a number with a decimal point becomes + * {@code Double}; an integer becomes {@code Long}. + */ + @Test + public void testNumberStrategyLongOrDouble() { + CommonRequest req = new CommonRequest("{\"Int\":1,\"Float\":1.5}"); + HashMap any = req.any(); + assertTrue("integer should be Long", any.get("Int") instanceof Long); + assertTrue("float should be Double", any.get("Float") instanceof Double); + assertEquals(1L, any.get("Int")); + assertEquals(1.5d, (Double) any.get("Float"), 0.0); + } + + // ================================================================= + // toMap flattening — invoked via the protected method from same package + // ================================================================= + + /** + * Calls {@link CommonRequest#toMap(HashMap, String)} with an empty prefix + * and returns the resulting flat map. + */ + private static HashMap flatten(CommonRequest req) { + HashMap map = new HashMap(); + req.toMap(map, ""); + return map; + } + + /** Top-level scalar key/value pairs flatten directly with no prefix. */ + @Test + public void testToMapFlatScalars() { + CommonRequest req = new CommonRequest("{\"Region\":\"ap-guangzhou\",\"Limit\":10}"); + HashMap map = flatten(req); + assertEquals("ap-guangzhou", map.get("Region")); + assertEquals("10", map.get("Limit")); + assertEquals(2, map.size()); + } + + /** Nested map keys are joined with a dot. */ + @Test + public void testToMapNestedMapUsesDotPrefix() { + CommonRequest req = new CommonRequest( + "{\"Filters\":{\"Name\":\"zone\",\"Values\":\"ap-gz1\"}}"); + HashMap map = flatten(req); + assertEquals("zone", map.get("Filters.Name")); + assertEquals("ap-gz1", map.get("Filters.Values")); + } + + /** List elements are indexed positionally with a dot before the index. */ + @Test + public void testToMapListUsesIndexPrefix() { + CommonRequest req = new CommonRequest( + "{\"Zones\":[\"ap-gz1\",\"ap-gz2\",\"ap-gz3\"]}"); + HashMap map = flatten(req); + assertEquals("ap-gz1", map.get("Zones.0")); + assertEquals("ap-gz2", map.get("Zones.1")); + assertEquals("ap-gz3", map.get("Zones.2")); + assertEquals(3, map.size()); + } + + /** Mixed nested map/list structures flatten with combined prefixes. */ + @Test + public void testToMapMixedNestedStructure() { + CommonRequest req = new CommonRequest( + "{\"Filters\":[{\"Name\":\"zone\",\"Values\":[\"ap-gz1\",\"ap-gz2\"]}]}"); + HashMap map = flatten(req); + assertEquals("Filters.0.Name", "zone", map.get("Filters.0.Name")); + assertEquals("ap-gz1", map.get("Filters.0.Values.0")); + assertEquals("ap-gz2", map.get("Filters.0.Values.1")); + assertEquals(3, map.size()); + } + + /** + * A null value anywhere in the structure is skipped — {@code toMapFromObject} + * returns early for null layers. + */ + @Test + public void testToMapNullValuesAreSkipped() { + CommonRequest req = new CommonRequest( + "{\"Keep\":\"v\",\"Drop\":null,\"Nested\":{\"A\":1,\"B\":null}}"); + HashMap map = flatten(req); + assertEquals("v", map.get("Keep")); + assertNull("null top-level value should be skipped", map.get("Drop")); + assertEquals("1", map.get("Nested.A")); + assertNull("null nested value should be skipped", map.get("Nested.B")); + assertEquals(2, map.size()); + } + + /** + * A non-empty prefix is prepended to every key (used when the parent + * calls toMap with a field-name prefix). + */ + @Test + public void testToMapWithNonEmptyPrefix() { + CommonRequest req = new CommonRequest("{\"A\":1,\"B\":2}"); + HashMap map = new HashMap(); + req.toMap(map, "Root"); + assertEquals("1", map.get("Root.A")); + assertEquals("2", map.get("Root.B")); + assertEquals(2, map.size()); + } + + /** + * Boolean leaf values are stringified via {@code toString()} → "true"/"false". + */ + @Test + public void testToMapBooleanLeafStringified() { + CommonRequest req = new CommonRequest("{\"Enabled\":true,\"Disabled\":false}"); + HashMap map = flatten(req); + assertEquals("true", map.get("Enabled")); + assertEquals("false", map.get("Disabled")); + } +} diff --git a/src/test/java/com/tencentcloudapi/common/EndpointFailoverInterceptorTest.java b/src/test/java/com/tencentcloudapi/common/EndpointFailoverInterceptorTest.java new file mode 100644 index 000000000..6ab7fb66e --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/EndpointFailoverInterceptorTest.java @@ -0,0 +1,1611 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.http.HttpConnection; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.cvm.v20170312.CvmClient; +import com.tencentcloudapi.cvm.v20170312.models.DescribeInstancesRequest; +import com.tencentcloudapi.cvm.v20170312.models.DescribeInstancesResponse; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import org.junit.Test; + +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests for {@link EndpointFailoverInterceptor}. + * + *

How tests are organized

+ * + *

Each test scenario is run against every domain family (N1/N2/N3) via a + * {@code for (Family f : FAMILIES)} loop inside the test method. This gives + * M × N coverage while keeping the code DRY. + * + *

Scenarios that do NOT depend on the domain family (e.g. pure helper + * methods like {@code isKnownTencentCloudHost}) use a single test method + * without the family loop. + * + *

Adding a new test case

+ * + *
    + *
  1. Choose the right section. Find the scenario group that best + * matches (see the M-numbered list below), or add a new group at the + * bottom. + *
  2. If family-dependent: Copy the pattern of an existing test in + * that section — wrap the test body in {@code for (Family f : FAMILIES)} + * and use {@code f.originHost}, {@code f.firstFailover}, etc. + * Include {@code f.name} in assertion messages for debugging. + *
  3. If family-independent: Write a plain {@code @Test} method + * without the loop. + *
  4. Use existing helpers: + *
      + *
    • {@link #newClient(Family)} — creates a CvmClient for a family + *
    • {@link #installStub(AbstractClient)} — installs TransportStub + *
    • {@link #tripBreakerFor(AbstractClient, String, long)} — trips a breaker + *
    • {@link #tripAllBreakersFor(AbstractClient, Family, long)} — trips all 3 + *
    • {@link #failoverInterceptorOf(AbstractClient)} — gets the interceptor + *
    + *
  5. Script the TransportStub: Use {@code transport.programOk()}, + * {@code transport.programFailure(ex)}, {@code transport.programJsonOk(json)}, + * {@code transport.programResponse(code, body)}, or + * {@code transport.programResponseWithCt(code, body, contentType)}. + *
  6. Assert transport hits: Check {@code transport.received.size()} + * and {@code transport.received.get(i).url().host()}. + *
  7. Assign a scenario number. Pick the next available M number and + * update the list below so the index stays current. + *
+ * + *

Domain families (N)

+ *
    + *
  • N1: Normal — {@code tencentcloudapi.{com,cn,com.cn}} + *
  • N2: AI — {@code ai.tencentcloudapi.{com,cn,com.cn}} + *
  • N3: Internal — {@code internal.tencentcloudapi.{com,cn,com.cn}} + *
+ * + *

Test scenarios (M)

+ * + *

Pure helpers

+ *
    + *
  1. isKnownTencentCloudHost — host classification + *
  2. hostWithTld / serviceOf — utility helpers + *
+ * + *

Pass-through

+ *
    + *
  1. Non-TencentCloud host — passthrough + *
  2. Unknown TLD — passthrough + *
  3. Non-POST request — passthrough + *
  4. Non-failover IOException — propagate without retry + *
+ * + *

Failover triggers

+ *
    + *
  1. UnknownHostException + *
  2. SSLPeerUnverifiedException + *
  3. SSLHandshakeException + *
  4. ConnectException + *
  5. NoRouteToHostException + *
  6. PortUnreachableException + *
  7. SocketTimeoutException + *
  8. HTTP non-200 response (UnhealthyResponseException) + *
+ * + *

Response after failover

+ *
    + *
  1. API response delivered intact after selecting alternate host + *
+ * + *

Circuit breaker lifecycle

+ *
    + *
  1. Sustained failure opens breaker + *
  2. Open breaker short-circuits host + *
  3. Open → HalfOpen after cooldown + *
  4. HalfOpen probe success → Closed + *
  5. HalfOpen probe failure → re-Open + *
  6. All breakers open → fallback to origin host + *
+ * + *

Failure reporting & isolation

+ *
    + *
  1. One transport attempt per request (no same-request retry) + *
  2. Failure preserves original exception type and message + *
  3. Breaker skip mixed with real failure + *
  4. Failure does not pollute next request + *
  5. Breaker state isolated across origin hosts + *
+ * + *

Request re-signing

+ *
    + *
  1. TC3 V3 re-sign (POST) + *
  2. TC3 V3 GET re-sign + *
  3. Hmac V1 re-sign + *
  4. SKIP V3 — rewrite Host only + *
  5. Re-sign uses current credential (SecretId/Key rotation) + *
  6. X-TC-Token rotation + *
+ * + *

Response type handling

+ *
    + *
  1. SSE (text/event-stream) — no failover + *
  2. No Content-Type — no failover + *
  3. 200 + JSON business error — no failover + *
+ * + *

backupEndpoint mode

+ *
    + *
  1. backupEndpoint failover behavior + *
+ * + *

TLD boundary & region-pinned

+ *
    + *
  1. hostWithTld from .cn / .com.cn origins + *
  2. hostWithTld preserves region prefix + *
+ * + *

Passthrough details

+ *
    + *
  1. Non-TencentCloud host DNS miss — no retry + *
  2. Non-TencentCloud host + backupEndpoint — no retry + *
+ * + *

Endpoint eligibility

+ *
    + *
  1. .cn origin eligible for failover + *
  2. Region-pinned host eligible for failover + *
  3. setEnableDomainFailover(false) at runtime — no effect + *
+ * + *

TLD family rotation

+ *
    + *
  1. AI family stays within ai.tencentcloudapi + *
  2. Internal family stays within internal.tencentcloudapi + *
  3. Region-pinned failover preserves prefix + *
+ * + *

Re-sign details

+ *
    + *
  1. TC3 resign preserves body bytes and Content-Type + *
  2. X-TC-Token dropped when cleared + *
+ * + *

backupEndpoint details

+ *
    + *
  1. Origin DNS miss — no same-request retry to backup + *
  2. Non-failover IOException propagates directly + *
  3. No backupEndpoint — DNS miss behavior + *
+ */ +public class EndpointFailoverInterceptorTest { + + // ================================================================= + // Domain families + // ================================================================= + + private static final Family N1 = new Family( + "Normal", "tencentcloudapi.com", + "cvm.tencentcloudapi.com", + "cvm.tencentcloudapi.com.cn", + "cvm.tencentcloudapi.cn"); + + private static final Family N2 = new Family( + "AI", "ai.tencentcloudapi.com", + "cvm.ai.tencentcloudapi.com", + "cvm.ai.tencentcloudapi.com.cn", + "cvm.ai.tencentcloudapi.cn"); + + private static final Family N3 = new Family( + "Internal", "internal.tencentcloudapi.com", + "cvm.internal.tencentcloudapi.com", + "cvm.internal.tencentcloudapi.com.cn", + "cvm.internal.tencentcloudapi.cn"); + + private static final Family[] FAMILIES = {N1, N2, N3}; + + private static class Family { + final String name; + final String rootDomain; + final String originHost; + final String firstFailover; + final String secondFailover; + final String[] allTldHosts; + + Family(String name, String rootDomain, String host0, String host1, String host2) { + this.name = name; + this.rootDomain = rootDomain; + this.originHost = host0; + this.firstFailover = host1; + this.secondFailover = host2; + this.allTldHosts = new String[]{host0, host1, host2}; + } + } + + // ================================================================= + // M1: isKnownTencentCloudHost — host classification + // ================================================================= + + @Test + public void testIsKnownTencentCloudHost() { + // Normal family (tencentcloudapi.com / .cn / .com.cn) + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.tencentcloudapi.com")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.tencentcloudapi.cn")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.tencentcloudapi.com.cn")); + // intl prefix maps to the normal family. + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.intl.tencentcloudapi.com")); + // Region-pinned. + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ap-shanghai.tencentcloudapi.com")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ap-shanghai.tencentcloudapi.cn")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ap-shanghai.tencentcloudapi.com.cn")); + + // AI family (ai.tencentcloudapi.com / .cn / .com.cn) + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ai.tencentcloudapi.com")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ai.tencentcloudapi.cn")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ai.tencentcloudapi.com.cn")); + // Region-pinned. + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ap-guangzhou.ai.tencentcloudapi.com")); + + // Internal family (internal.tencentcloudapi.com / .cn / .com.cn) + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.internal.tencentcloudapi.com")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.internal.tencentcloudapi.cn")); + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.internal.tencentcloudapi.com.cn")); + // Region-pinned. + assertTrue(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.ap-guangzhou.internal.tencentcloudapi.com")); + + // Empty prefix (no service label) + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("tencentcloudapi.com")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("tencentcloudapi.cn")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("tencentcloudapi.com.cn")); + + // Malformed prefix + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost(".tencentcloudapi.com")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost(".foo.tencentcloudapi.com")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("foo..tencentcloudapi.com")); + + // Non-TencentCloud hosts + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("example.com")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("cvm.tencentcloudapi.woa.com")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("proxy.internal")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost("192.168.0.1")); + assertFalse(EndpointFailoverInterceptor.isKnownTencentCloudHost(null)); + } + + // ================================================================= + // M2: hostWithTld / serviceOf — utility helpers + // ================================================================= + + @Test + public void testHostWithTldBuildsCorrectHosts() { + assertEquals("cvm.tencentcloudapi.com", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.com", 0)); + assertEquals("cvm.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.com", 1)); + assertEquals("cvm.tencentcloudapi.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.com", 2)); + + assertEquals("cvm.ai.tencentcloudapi.com", + EndpointFailoverInterceptor.hostWithTld("cvm.ai.tencentcloudapi.com", 0)); + assertEquals("cvm.ai.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.ai.tencentcloudapi.com", 1)); + + assertEquals("cvm.internal.tencentcloudapi.com", + EndpointFailoverInterceptor.hostWithTld("cvm.internal.tencentcloudapi.com", 0)); + } + + @Test + public void testMatchFamilyReturnsNullForUnknownHosts() { + assertNull(EndpointFailoverInterceptor.matchFamily("example.com")); + assertNull(EndpointFailoverInterceptor.matchFamily(null)); + } + + @Test + public void testMatchFamilyReturnsFirstMatchingFamily() { + EndpointFailoverInterceptor.Match m; + + m = EndpointFailoverInterceptor.matchFamily("cvm.tencentcloudapi.com"); + assertNotNull(m); + assertEquals("cvm", m.prefix); + assertEquals("tencentcloudapi.com", m.family[0]); + + m = EndpointFailoverInterceptor.matchFamily("cvm.ai.tencentcloudapi.cn"); + assertNotNull(m); + assertEquals("cvm", m.prefix); + assertEquals("ai.tencentcloudapi.com", m.family[0]); + + m = EndpointFailoverInterceptor.matchFamily("cvm.internal.tencentcloudapi.com.cn"); + assertNotNull(m); + assertEquals("cvm", m.prefix); + assertEquals("internal.tencentcloudapi.com", m.family[0]); + } + + // intl prefix (cvm.intl.tencentcloudapi.com) maps to the normal family, + // with "intl" folded into the prefix rather than treated as a TLD. + @Test + public void testMatchFamilyHandlesIntlPrefix() { + EndpointFailoverInterceptor.Match m = + EndpointFailoverInterceptor.matchFamily("cvm.intl.tencentcloudapi.com"); + assertNotNull(m); + assertEquals("cvm.intl", m.prefix); + assertEquals("tencentcloudapi.com", m.family[0]); + assertEquals(0, m.tldIdx); + } + + @Test + public void testHostWithTldDropsIntlPrefix() { + assertEquals("cvm.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.intl.tencentcloudapi.com", 1)); + assertEquals("cvm.tencentcloudapi.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.intl.tencentcloudapi.com", 2)); + } + + // End-to-end: an intl-prefixed origin host rotates within the normal + // family (.com -> .com.cn -> .cn), same as a plain public host. + @Test + public void testIntlHostFailoverRotatesWithinNormalFamily() throws Exception { + Family f = new Family("Intl", "tencentcloudapi.com", + "cvm.intl.tencentcloudapi.com", + "cvm.tencentcloudapi.com.cn", + "cvm.tencentcloudapi.cn"); + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(1, transport.received.size()); + assertEquals(f.firstFailover, transport.received.get(0).url().host()); + } + + // ================================================================= + // M2a: hostWithTld — boundary origins (.cn, .com.cn, region-pinned) + // ================================================================= + + @Test + public void testHostWithTldFromCnAndComCnOrigins() { + // From .cn origin (tldIdx=2): idx 0 -> .com, idx 2 -> .cn + assertEquals("cvm.tencentcloudapi.com", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.cn", 0)); + assertEquals("cvm.tencentcloudapi.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.cn", 2)); + + // From .com.cn origin (tldIdx=1): idx 0 -> .com, idx 1 -> .com.cn + assertEquals("cvm.tencentcloudapi.com", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.com.cn", 0)); + assertEquals("cvm.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.tencentcloudapi.com.cn", 1)); + } + + @Test + public void testHostWithTldDropsRegionInPrefix() { + assertEquals("cvm.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.ap-guangzhou.tencentcloudapi.com", 1)); + assertEquals("cvm.ai.tencentcloudapi.com.cn", + EndpointFailoverInterceptor.hostWithTld("cvm.ap-guangzhou.ai.tencentcloudapi.com", 1)); + } + + // ================================================================= + // M3: Non-TencentCloud host — passthrough (family loop) + // ================================================================= + + @Test + public void testPassthroughNonTencentCloudHost() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + // Build a request to a non-TencentCloud host. + OkHttpClient http = grabOkHttpClient(client); + Request req = new Request.Builder() + .url("https://example.com/") + .header("Host", "example.com") + .header("Authorization", "SKIP") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + transport.programOk(); + Response resp = http.newCall(req).execute(); + resp.close(); + assertEquals(1, transport.received.size()); + assertEquals("example.com", transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M3a: Non-TencentCloud host — DNS miss + backup details + // ================================================================= + + @Test + public void testNonTencentHostDnsMissPropagatesWithoutRetry() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("proxy dns miss")); + + OkHttpClient http = grabOkHttpClient(client); + Request req = new Request.Builder() + .url("https://proxy.internal/") + .header("Host", "proxy.internal") + .header("Authorization", "SKIP") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + + try { + http.newCall(req).execute(); + fail(f.name + ": expected IOException"); + } catch (IOException e) { + assertTrue(f.name, e instanceof UnknownHostException); + } + assertEquals(f.name + ": no retry for non-TencentCloud host", + 1, transport.received.size()); + } + } + + @Test + public void testNonTencentHostWithBackupDoesNotRetrySameRequest() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + profile.setBackupEndpoint("backup.example.com"); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("proxy dns miss")); + + OkHttpClient http = grabOkHttpClient(client); + Request req = new Request.Builder() + .url("https://proxy.internal/") + .header("Host", "proxy.internal") + .header("Authorization", "SKIP") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + + try { + http.newCall(req).execute(); + fail(f.name + ": expected IOException"); + } catch (IOException ignored) { } + assertEquals(f.name + ": only one attempt, no retry to backup", + 1, transport.received.size()); + } + } + + // ================================================================= + // M4: Unknown TLD — passthrough (family loop) + // ================================================================= + + @Test + public void testPassthroughUnknownTld() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M5: Non-POST request — passthrough (family loop) + // ================================================================= + + @Test + public void testPassthroughNonPostRequest() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setReqMethod(HttpProfile.REQ_GET); + profile.getHttpProfile().setEndpoint(f.originHost); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M6: Non-failover IOException — propagate without retry (family loop) + // ================================================================= + + @Test + public void testGenericIOExceptionPropagatesWithoutFailover() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new IOException("unrelated I/O error")); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + assertTrue(f.name + ": expected IOException cause", + e.getCause() instanceof IOException); + assertEquals(f.name + ": should not retry", 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + } + + // ================================================================= + // M6a-M6d: Endpoint eligibility (family loop) + // ================================================================= + + @Test + public void testCnOriginIsEligibleForFailover() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("cn dns miss")); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + @Test + public void testRegionPinnedHostIsEligibleForFailover() throws Exception { + // Use region-pinned hosts: cvm.ap-guangzhou.tencentcloudapi.com etc. + Family[] regionFamilies = { + new Family("Normal-R", "tencentcloudapi.com", + "cvm.ap-guangzhou.tencentcloudapi.com", + "cvm.tencentcloudapi.com.cn", + "cvm.tencentcloudapi.cn"), + new Family("AI-R", "ai.tencentcloudapi.com", + "cvm.ap-guangzhou.ai.tencentcloudapi.com", + "cvm.ai.tencentcloudapi.com.cn", + "cvm.ai.tencentcloudapi.cn"), + new Family("Internal-R", "internal.tencentcloudapi.com", + "cvm.ap-guangzhou.internal.tencentcloudapi.com", + "cvm.internal.tencentcloudapi.com.cn", + "cvm.internal.tencentcloudapi.cn"), + }; + for (Family f : regionFamilies) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("region dns miss")); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + @Test + public void testFailoverEnabledAtRuntimeHasNoEffect() throws Exception { + // setEnableDomainFailover(false) after constructor: interceptor is already installed. + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + client.getClientProfile().setEnableDomainFailover(false); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("dns miss")); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name + ": interceptor is already installed, flag has no effect", + 1, transport.received.size()); + } + } + + // ================================================================= + // M7-M13: Failover IOException types (family loop) + // ================================================================= + + @Test public void testFailoverOnUnknownHostException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new UnknownHostException("dns miss")); + } + + @Test public void testFailoverOnSslPeerUnverifiedException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new SSLPeerUnverifiedException("cert mismatch")); + } + + @Test public void testFailoverOnSslHandshakeException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new SSLHandshakeException("tls fail")); + } + + @Test public void testFailoverOnConnectException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new ConnectException("connection refused")); + } + + @Test public void testFailoverOnNoRouteToHostException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new NoRouteToHostException("no route")); + } + + @Test public void testFailoverOnPortUnreachableException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new java.net.PortUnreachableException("port unreachable")); + } + + @Test public void testFailoverOnSocketTimeoutException() throws Exception { + for (Family f : FAMILIES) runFailoverFor(f, new SocketTimeoutException("read timed out")); + } + + private void runFailoverFor(Family f, IOException failure) throws Exception { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(failure); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name + ": one transport attempt per request", + 1, transport.received.size()); + assertEquals(f.name + ": first attempt hits origin", + f.originHost, transport.received.get(0).url().host()); + } + + // ================================================================= + // M14: Protocol-level failover (family loop) + // ================================================================= + + @Test + public void testNon200ResponseRecordsFailureWithoutRetry() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programResponse(503, "{\"Response\":{\"Error\":{}}}"); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M16a-M16b: TLD family rotation (AI/Internal don't cross families) + // ================================================================= + + @Test + public void testAiFamilyRotationStaysWithinFamily() throws Exception { + Family f = N2; + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(1, transport.received.size()); + assertEquals(f.firstFailover, transport.received.get(0).url().host()); + // Must be within ai.tencentcloudapi family, NOT plain cvm.tencentcloudapi.cn + assertTrue(transport.received.get(0).url().host().contains("ai.tencentcloudapi")); + } + + @Test + public void testInternalFamilyRotationStaysWithinFamily() throws Exception { + Family f = N3; + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(1, transport.received.size()); + assertEquals(f.firstFailover, transport.received.get(0).url().host()); + assertTrue(transport.received.get(0).url().host().contains("internal.tencentcloudapi")); + } + + @Test + public void testRegionPinnedHostFailoverDropsPrefix() throws Exception { + Family f = new Family("R", "tencentcloudapi.com", + "cvm.ap-guangzhou.tencentcloudapi.com", + "cvm.tencentcloudapi.com.cn", + "cvm.tencentcloudapi.cn"); + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(1, transport.received.size()); + assertEquals(f.firstFailover, transport.received.get(0).url().host()); + assertFalse(transport.received.get(0).url().host().contains("ap-guangzhou")); + } + + // ================================================================= + // M17: API response delivered after failover (family loop) + // ================================================================= + + @Test + public void testApiResponseDeliveredAfterFailover() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programJsonOk("{\"Response\":{\"TotalCount\":42,\"InstanceSet\":[],\"RequestId\":\"req-xyz\"}}"); + + DescribeInstancesResponse resp = client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, Long.valueOf(42), resp.getTotalCount()); + assertEquals(f.name, "req-xyz", resp.getRequestId()); + assertEquals(f.name, f.firstFailover, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M18: Sustained failure opens breaker (family loop) + // ================================================================= + + @Test + public void testBreakerOpensAfterSustainedFailure() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + + for (int i = 0; i < 5; i++) { + transport.programFailure(new UnknownHostException("fail " + i)); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + } + assertEquals(f.name, 5, transport.received.size()); + + assertFalse(f.name + ": origin breaker should be Open", + failoverInterceptorOf(client).breakerFor(f.originHost).allow().allowed); + + // Next request short-circuits origin, goes to first failover. + transport.received.clear(); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.firstFailover, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M19: Open breaker short-circuits host (family loop) + // ================================================================= + + @Test + public void testOpenBreakerShortCircuitsHost() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name + ": should skip origin and hit first failover", + f.firstFailover, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M20: Open → HalfOpen after cooldown (family loop) + // ================================================================= + + @Test + public void testBreakerTransitionsOpenToHalfOpenAfterCooldown() throws Exception { + long shortTimeoutMs = 100; + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + CircuitBreaker breaker = tripBreakerFor(client, f.originHost, shortTimeoutMs); + assertFalse(f.name, breaker.allow().allowed); + + Thread.sleep(shortTimeoutMs + 50); + CircuitBreaker.Token probe = breaker.allow(); + assertTrue(f.name + ": should permit HalfOpen probe", probe.allowed); + } + } + + // ================================================================= + // M21: HalfOpen probe success → Closed (family loop) + // ================================================================= + + @Test + public void testBreakerReClosesAfterHalfOpenSuccess() throws Exception { + long shortTimeoutMs = 100; + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + CircuitBreaker breaker = tripBreakerFor(client, f.originHost, shortTimeoutMs); + TransportStub transport = installStub(client); + + Thread.sleep(shortTimeoutMs + 50); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + + for (int i = 0; i < 10; i++) { + assertTrue(f.name + ": should be Closed after HalfOpen success", + breaker.allow().allowed); + } + } + } + + // ================================================================= + // M22: HalfOpen probe failure → re-Open (family loop) + // ================================================================= + + @Test + public void testBreakerReOpensWhenHalfOpenProbeFails() throws Exception { + long shortTimeoutMs = 100; + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + CircuitBreaker breaker = tripBreakerFor(client, f.originHost, shortTimeoutMs); + TransportStub transport = installStub(client); + + Thread.sleep(shortTimeoutMs + 50); + transport.programFailure(new UnknownHostException("still down")); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + + assertFalse(f.name + ": should re-Open after HalfOpen failure", + breaker.allow().allowed); + + // Next request short-circuits origin again. + transport.received.clear(); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.firstFailover, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M23: All breakers open → fallback to origin host (family loop) + // ================================================================= + + @Test + public void testAllBreakersOpenFallsBackToOriginHost() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripAllBreakersFor(client, f, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M23a: Candidate rotation follows family TLD order, not just the + // first candidate. Trip origin AND firstFailover's breakers, leaving + // only secondFailover closed — the request must land on secondFailover, + // proving buildCandidateHosts walks the family array in order + // ((tldIdx+1)%len, (tldIdx+2)%len, ...) rather than stopping/looping + // incorrectly after the first entry. + // ================================================================= + + @Test + public void testFailoverAdvancesToSecondCandidateWhenFirstIsAlsoOpen() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + tripBreakerFor(client, f.firstFailover, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name + ": origin and first candidate open, must advance to second", + f.secondFailover, transport.received.get(0).url().host()); + } + } + + // Same idea, but verified directly against selectHost's returned + // Candidate rather than through a full DescribeInstances round-trip — + // pins down the exact rotation order independent of request signing. + @Test + public void testSelectHostWalksCandidatesInFamilyOrder() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + EndpointFailoverInterceptor interceptor = failoverInterceptorOf(client); + Request req = new Request.Builder() + .url("https://" + f.originHost + "/") + .header("Host", f.originHost) + .build(); + + // All breakers closed: origin wins. + assertEquals(f.name, f.originHost, interceptor.selectHost(req).host); + + // Origin open: first candidate (tldIdx+1) wins. + tripBreakerFor(client, f.originHost, 60_000); + assertEquals(f.name, f.firstFailover, interceptor.selectHost(req).host); + + // Origin + first open: second candidate (tldIdx+2) wins. + tripBreakerFor(client, f.firstFailover, 60_000); + assertEquals(f.name, f.secondFailover, interceptor.selectHost(req).host); + + // All three open: falls back to origin. + tripBreakerFor(client, f.secondFailover, 60_000); + assertEquals(f.name, f.originHost, interceptor.selectHost(req).host); + } + } + + // ================================================================= + // M24: One transport attempt per request (family loop) + // ================================================================= + + @Test + public void testEndpointFailureSurfacesAttemptFailure() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("dns miss " + f.name)); + + TencentCloudSDKException sdkEx = null; + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + sdkEx = e; + } + IOException primary = unwrapToIOException(sdkEx); + assertTrue(f.name, primary.getMessage().contains("dns miss")); + assertEquals(f.name, 0, primary.getSuppressed().length); + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M25: Failure preserves original exception type and message (family loop) + // ================================================================= + + @Test + public void testFailurePreservesAttemptCauseType() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new ConnectException("connect fail " + f.name)); + + TencentCloudSDKException sdkEx = null; + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + sdkEx = e; + } + IOException primary = unwrapToIOException(sdkEx); + assertTrue(f.name, primary instanceof ConnectException); + assertEquals(f.name, 0, primary.getSuppressed().length); + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M26: Breaker skip mixed with real failure (family loop) + // ================================================================= + + @Test + public void testFailureMixesPriorBreakerSkipsWithRealFailure() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programFailure(new SSLHandshakeException("tls fail")); + + TencentCloudSDKException sdkEx = null; + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + sdkEx = e; + } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name + ": origin skipped, hits first failover", + f.firstFailover, transport.received.get(0).url().host()); + + IOException primary = unwrapToIOException(sdkEx); + assertTrue(f.name, primary instanceof SSLHandshakeException); + assertEquals(f.name, 0, primary.getSuppressed().length); + } + } + + // ================================================================= + // M27: Failure does not pollute next request (family loop) + // ================================================================= + + @Test + public void testFailoverDoesNotPolluteNextRequest() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + + transport.programFailure(new UnknownHostException("run1 " + f.name)); + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + transport.received.clear(); + + transport.programFailure(new UnknownHostException("run2 " + f.name)); + TencentCloudSDKException sdkEx = null; + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + sdkEx = e; + } + IOException primary = unwrapToIOException(sdkEx); + assertEquals(f.name, 0, primary.getSuppressed().length); + assertTrue(f.name, primary.getMessage().contains("run2")); + assertFalse(f.name, primary.getMessage().contains("run1")); + } + } + + // ================================================================= + // M28: Breaker state isolated across origin hosts (family loop) + // ================================================================= + + @Test + public void testBreakerStateIsolatedAcrossOriginHosts() throws Exception { + // Trip breaker for one family, verify it doesn't affect another family. + // We test isolation between N1 and N2, and between N1 and N3. + Family[][] pairs = {{N1, N2}, {N1, N3}}; + for (Family[] pair : pairs) { + Family a = pair[0]; + Family b = pair[1]; + + CvmClient clientA = newClient(a); + tripBreakerFor(clientA, a.originHost, 60_000); + + CvmClient clientB = newClient(b); + TransportStub transportB = installStub(clientB); + transportB.programOk(); + clientB.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(b.name + " should be unaffected by " + a.name, + 1, transportB.received.size()); + assertEquals(b.name, b.originHost, transportB.received.get(0).url().host()); + } + } + + // ================================================================= + // M29: TC3 V3 re-sign (POST) (family loop) + // ================================================================= + + @Test + public void testTc3ResignOnFailover() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + Request resigned = transport.received.get(0); + assertEquals(f.name, f.firstFailover, resigned.url().host()); + assertTrue(f.name, resigned.header("Authorization").contains("TC3-HMAC-SHA256")); + } + } + + // ================================================================= + // M29a: TC3 resign preserves body and content-type + // ================================================================= + + @Test + public void testTc3ResignPreservesBodyAndContentType() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + Request resigned = transport.received.get(0); + assertEquals(f.name, f.firstFailover, resigned.url().host()); + assertEquals(f.name, "application/json", resigned.header("Content-Type")); + byte[] body = bodyBytes(resigned); + assertTrue(f.name, body.length > 0); + } + } + + // ================================================================= + // M30: TC3 V3 GET re-sign (family loop) + // ================================================================= + + @Test + public void testTc3GetResignOnFailover() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setReqMethod(HttpProfile.REQ_GET); + profile.getHttpProfile().setEndpoint(f.originHost); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + Request resigned = transport.received.get(0); + assertEquals(f.name, f.firstFailover, resigned.url().host()); + assertEquals(f.name, "GET", resigned.method()); + assertTrue(f.name, resigned.header("Authorization").contains("TC3-HMAC-SHA256")); + } + } + + // ================================================================= + // M31: Hmac V1 re-sign (family loop) + // ================================================================= + + @Test + public void testHmacV1ResignOnFailover() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.setSignMethod(ClientProfile.SIGN_SHA1); + profile.getHttpProfile().setEndpoint(f.originHost); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + Request resigned = transport.received.get(0); + assertEquals(f.name, f.firstFailover, resigned.url().host()); + // Hmac V1 embeds Signature in body, not Authorization header. + byte[] body = bodyBytes(resigned); + assertTrue(f.name, new String(body).contains("Signature=")); + } + } + + // ================================================================= + // M32: SKIP V3 — rewrite Host only (family loop) + // ================================================================= + + @Test + public void testSkipSignV3OnFailoverRewritesHostWithoutResigning() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + OkHttpClient http = grabOkHttpClient(client); + Request raw = new Request.Builder() + .url("https://" + f.originHost + "/") + .header("Host", f.originHost) + .header("Authorization", "SKIP") + .header("X-TC-Action", "DescribeInstances") + .header("X-TC-Version", "2017-03-12") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + + Response resp = http.newCall(raw).execute(); + resp.close(); + assertEquals(f.name, 1, transport.received.size()); + Request resigned = transport.received.get(0); + assertEquals(f.name, f.firstFailover, resigned.url().host()); + assertEquals(f.name, f.firstFailover, resigned.header("Host")); + assertEquals(f.name, "SKIP", resigned.header("Authorization")); + } + } + + // ================================================================= + // M33: Re-sign uses current credential (family loop) + // ================================================================= + + @Test + public void testResignUsesCurrentCredential() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + client.setCredential(new Credential("AKIDNEW", "SKNEW")); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertTrue(f.name, transport.received.get(0).header("Authorization") + .contains("Credential=AKIDNEW/")); + } + } + + // ================================================================= + // M34: X-TC-Token rotation (family loop) + // ================================================================= + + @Test + public void testXtcTokenRotationOnFailover() throws Exception { + for (Family f : FAMILIES) { + Credential cred = new Credential("AKIDTEST", "SKTEST", "token-abc"); + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + CvmClient client = new CvmClient(cred, "ap-guangzhou", profile); + tripBreakerFor(client, f.originHost, 60_000); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, "token-abc", transport.received.get(0).header("X-TC-Token")); + } + } + + // ================================================================= + // M34a: X-TC-Token dropped when cleared + // ================================================================= + + @Test + public void testResignDropsTokenWhenCleared() throws Exception { + for (Family f : FAMILIES) { + Credential cred = new Credential("AKIDTEST", "SKTEST", "token-abc"); + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + CvmClient client = new CvmClient(cred, "ap-guangzhou", profile); + tripBreakerFor(client, f.originHost, 60_000); + // Clear the token before the failover request. + client.setCredential(new Credential("AKIDTEST", "SKTEST")); + TransportStub transport = installStub(client); + transport.programOk(); + + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name, 1, transport.received.size()); + assertNull(f.name, transport.received.get(0).header("X-TC-Token")); + } + } + + // ================================================================= + // M35: SSE — no failover (family loop) + // ================================================================= + + @Test + public void testSseStreamResponseIsNotJsonValidated() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programResponseWithCt(200, "data: hello\n\n", "text/event-stream"); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + } catch (Exception ignored) { } + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M36: No Content-Type — no failover (family loop) + // ================================================================= + + @Test + public void testResponseWithoutContentTypeIsNotJsonValidated() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programResponseWithCt(200, "oops", null); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + } catch (Exception ignored) { } + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M37: 200 + JSON business error — no failover (family loop) + // ================================================================= + + @Test + public void testBusinessSdkErrorDoesNotTriggerFailover() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programJsonOk( + "{\"Response\":{\"RequestId\":\"req-bad\",\"Error\":{" + + "\"Code\":\"AuthFailure.SignatureFailure\"," + + "\"Message\":\"signature wrong\"}}}"); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected business SDK exception"); + } catch (TencentCloudSDKException e) { + assertEquals(f.name, "AuthFailure.SignatureFailure", e.getErrorCode()); + assertEquals(f.name, "req-bad", e.getRequestId()); + } + assertEquals(f.name, 1, transport.received.size()); + } + } + + // ================================================================= + // M38: backupEndpoint failover behavior (family loop) + // ================================================================= + + @Test + public void testBackupEndpointFailover() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + profile.setBackupEndpoint("backup.example.com"); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + TransportStub transport = installStub(client); + + // Origin succeeds. + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name + " origin", 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + + // Trip origin breaker, backup should be used. + tripBreakerFor(client, f.originHost, 60_000); + transport.received.clear(); + transport.programOk(); + client.DescribeInstances(new DescribeInstancesRequest()); + assertEquals(f.name + " backup", 1, transport.received.size()); + String servicePrefix = f.originHost.substring(0, f.originHost.indexOf('.')); + String backupHost = servicePrefix + ".backup.example.com"; + assertEquals(f.name, backupHost, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // M38a-M38d: backupEndpoint detail scenarios (family loop) + // ================================================================= + + @Test + public void testBackupEndpointOriginDnsMissDoesNotRetrySameRequest() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + profile.setBackupEndpoint("backup.example.com"); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("origin dns miss")); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name + ": no same-request retry to backup", + 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + @Test + public void testBackupEndpointNonFailoverErrorPropagates() throws Exception { + for (Family f : FAMILIES) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(f.originHost); + profile.setBackupEndpoint("backup.example.com"); + CvmClient client = new CvmClient( + new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + TransportStub transport = installStub(client); + transport.programFailure(new IOException("generic I/O error")); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException e) { + assertTrue(f.name, e.getCause() instanceof IOException); + } + assertEquals(f.name, 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + @Test + public void testNoBackupEndpointDnsMissDoesNotRetrySameRequest() throws Exception { + for (Family f : FAMILIES) { + CvmClient client = newClient(f); + TransportStub transport = installStub(client); + transport.programFailure(new UnknownHostException("dns miss")); + + try { + client.DescribeInstances(new DescribeInstancesRequest()); + fail(f.name + ": expected SDK exception"); + } catch (TencentCloudSDKException ignored) { } + assertEquals(f.name + ": no same-request retry", 1, transport.received.size()); + assertEquals(f.name, f.originHost, transport.received.get(0).url().host()); + } + } + + // ================================================================= + // Helpers + // ================================================================= + + private static CvmClient newClient(Family family) { + ClientProfile profile = new ClientProfile(); + profile.getHttpProfile().setEndpoint(family.originHost); + return new CvmClient(new Credential("AKIDTEST", "SKTEST"), "ap-guangzhou", profile); + } + + private static TransportStub installStub(AbstractClient client) { + TransportStub stub = new TransportStub(); + OkHttpClient orig = grabOkHttpClient(client); + setOkHttpClient(client, orig.newBuilder().addInterceptor(stub).build()); + return stub; + } + + private static OkHttpClient grabOkHttpClient(AbstractClient client) { + try { + Field f = AbstractClient.class.getDeclaredField("httpConnection"); + f.setAccessible(true); + HttpConnection conn = (HttpConnection) f.get(client); + return (OkHttpClient) conn.getHttpClient(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static void setOkHttpClient(AbstractClient client, OkHttpClient http) { + try { + Field f = AbstractClient.class.getDeclaredField("httpConnection"); + f.setAccessible(true); + HttpConnection conn = (HttpConnection) f.get(client); + conn.setHttpClient(http); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static EndpointFailoverInterceptor failoverInterceptorOf(AbstractClient client) { + for (Interceptor it : grabOkHttpClient(client).interceptors()) { + if (it instanceof EndpointFailoverInterceptor) { + return (EndpointFailoverInterceptor) it; + } + } + throw new IllegalStateException("EndpointFailoverInterceptor not installed on client"); + } + + private static CircuitBreaker tripBreakerFor(AbstractClient client, String host, long timeoutMs) { + CircuitBreaker breaker = newBreaker(timeoutMs); + failoverInterceptorOf(client).putBreakerForTesting(host, breaker); + tripBreaker(breaker); + return breaker; + } + + private static void tripAllBreakersFor(AbstractClient client, Family f, long timeoutMs) { + for (String host : f.allTldHosts) { + tripBreakerFor(client, host, timeoutMs); + } + } + + private static CircuitBreaker newBreaker(long timeoutMs) { + CircuitBreaker.Setting setting = new CircuitBreaker.Setting(); + setting.timeoutMs = timeoutMs; + return new CircuitBreaker(setting); + } + + private static void tripBreaker(CircuitBreaker breaker) { + for (int i = 0; i < 6; i++) { + CircuitBreaker.Token t = breaker.allow(); + if (t.allowed) { + t.report(false); + } + } + } + + private static IOException unwrapToIOException(TencentCloudSDKException e) { + Throwable cause = e.getCause(); + assertNotNull("SDK exception must wrap an IOException, got null cause", cause); + assertTrue("expected IOException cause, got " + cause.getClass().getName(), + cause instanceof IOException); + return (IOException) cause; + } + + private static byte[] bodyBytes(Request req) throws IOException { + if (req.body() == null) { + return new byte[0]; + } + Buffer buf = new Buffer(); + req.body().writeTo(buf); + return buf.readByteArray(); + } + + // ================================================================= + // TransportStub + // ================================================================= + + private static final class TransportStub implements Interceptor { + final List received = new ArrayList(); + private final Queue programmed = new LinkedList(); + + void programFailure(IOException e) { + programmed.add(e); + } + + void programOk() { + programJsonOk("{\"Response\":{\"RequestId\":\"req-ok\"}}"); + } + + void programJsonOk(String json) { + programmed.add(new ProgrammedResponse(200, json, "application/json")); + } + + void programResponse(int code, String body) { + programmed.add(new ProgrammedResponse(code, body, "application/json")); + } + + void programResponseWithCt(int code, String body, String contentType) { + programmed.add(new ProgrammedResponse(code, body, contentType)); + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + received.add(request); + Object next = programmed.poll(); + if (next == null) { + throw new IllegalStateException( + "TransportStub got an unexpected request to " + + request.url() + " — no programmed outcome left"); + } + if (next instanceof IOException) { + throw (IOException) next; + } + ProgrammedResponse pr = (ProgrammedResponse) next; + Response.Builder b = new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(pr.code) + .message(pr.code == 200 ? "OK" : "Error"); + if (pr.contentType != null) { + b.header("Content-Type", pr.contentType); + b.body(ResponseBody.create(MediaType.parse(pr.contentType), pr.body)); + } else { + b.body(ResponseBody.create(null, pr.body)); + } + return b.build(); + } + + private static final class ProgrammedResponse { + final int code; + final String body; + final String contentType; + + ProgrammedResponse(int code, String body, String contentType) { + this.code = code; + this.body = body; + this.contentType = contentType; + } + } + } +} diff --git a/src/test/java/com/tencentcloudapi/common/RequestBuilderTest.java b/src/test/java/com/tencentcloudapi/common/RequestBuilderTest.java new file mode 100644 index 000000000..0cb5a93a6 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/RequestBuilderTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencentcloudapi.common; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.junit.Test; + +import java.io.IOException; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests for {@link RequestBuilder}. + * + *

Covers the three signing paths (TC3, v1, skip), the URL/host separation + * ({@code withURL} decoupling the connection target from the signature host), + * default-host fallback when {@code withHost} is not called, and the {@code build()} + * validation failures. + */ +public class RequestBuilderTest { + + private static final String ORIGIN_HOST = "cvm.tencentcloudapi.com"; + private static final String FAILOVER_HOST = "cvm.internal.tencentcloudapi.com"; + private static final Credential CRED = new Credential("AKIDTEST", "SKTEST"); + + // ================================================================= + // Helpers + // ================================================================= + + /** Builds a signed origin request so tests start from a realistic input. */ + private static Request signedOriginRequest() throws IOException, TencentCloudSDKException { + return new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(ORIGIN_HOST) + .build(); + } + + private static Request newPostRequest(String urlHost, String headerHost) { + return new Request.Builder() + .url("https://" + urlHost + "/") + .header("Host", headerHost) + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + } + + /** GET request with a query string (for v1 sign tests). */ + private static Request newGetRequest(String urlHost, String headerHost) { + return new Request.Builder() + .url("https://" + urlHost + "/?Action=DescribeInstances&Version=2017-03-12") + .header("Host", headerHost) + .get() + .build(); + } + + private static Request newFormPostRequest(String urlHost, String headerHost) { + return new Request.Builder() + .url("https://" + urlHost + "/") + .header("Host", headerHost) + .header("Content-Type", "application/x-www-form-urlencoded") + .post(RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), + "Action=DescribeInstances&Version=2017-03-12".getBytes())) + .build(); + } + + private static Request newSkipRequest(String urlHost) { + return new Request.Builder() + .url("https://" + urlHost + "/") + .header("Host", urlHost) + .header("Authorization", "SKIP") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + } + + // ================================================================= + // build() validation + // ================================================================= + + @Test + public void testBuildThrowsWhenCredentialMissing() throws IOException { + try { + new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(FAILOVER_HOST) + .build(); + fail("expected TencentCloudSDKException"); + } catch (TencentCloudSDKException e) { + assertTrue(e.getMessage().contains("credential")); + } + } + + @Test + public void testBuildThrowsWhenSignMethodMissing() throws IOException { + try { + new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withHost(FAILOVER_HOST) + .build(); + fail("expected TencentCloudSDKException"); + } catch (TencentCloudSDKException e) { + assertTrue(e.getMessage().contains("sign method")); + } + } + + // ================================================================= + // TC3 signing + host rewrite + // ================================================================= + + // withUrlHost + withHost rewrites both URL host and Host header to the target. + @Test + public void testWithHostRewritesUrlAndHeader() throws Exception { + Request out = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withUrlHost(FAILOVER_HOST) + .withHost(FAILOVER_HOST) + .build(); + + assertEquals(FAILOVER_HOST, out.url().host()); + assertEquals(443, out.url().port()); + assertEquals(FAILOVER_HOST, out.header("Host")); + assertNotNull(out.header("Authorization")); + assertNotNull(out.header("X-TC-Timestamp")); + } + + // Re-signing for a different host produces a different signature. + @Test + public void testReSignProducesDifferentAuthorization() throws Exception { + Request origin = signedOriginRequest(); + String originAuth = origin.header("Authorization"); + + Request resigned = new RequestBuilder(origin) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(FAILOVER_HOST) + .build(); + + assertNotEquals(originAuth, resigned.header("Authorization")); + assertEquals(FAILOVER_HOST, resigned.header("Host")); + } + + // Token in credential is propagated to X-TC-Token. + @Test + public void testTokenPropagatedToHeader() throws Exception { + Credential withToken = new Credential("AKIDTEST", "SKTEST", "token-value"); + Request out = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(withToken) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(FAILOVER_HOST) + .build(); + assertEquals("token-value", out.header("X-TC-Token")); + } + + // Empty token removes the X-TC-Token header. + @Test + public void testEmptyTokenRemovesHeader() throws Exception { + Request origin = new Request.Builder() + .url("https://" + ORIGIN_HOST + "/") + .header("Host", ORIGIN_HOST) + .header("X-TC-Token", "stale") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + Request out = new RequestBuilder(origin) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(FAILOVER_HOST) + .build(); + assertNull(out.header("X-TC-Token")); + } + + @Test + public void testExplicitServiceUsedInTc3CredentialScope() throws Exception { + Request out = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost("gateway.example.com") + .withService("cvm") + .build(); + + assertTrue(out.header("Authorization").contains("/cvm/tc3_request")); + } + + // Unsigned-payload flag is reflected in the canonical request hash. + @Test + public void testUnsignedPayloadChangesAuthorization() throws Exception { + Request signed = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(FAILOVER_HOST) + .build(); + Request unsigned = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withUnsignedPayload(true) + .withHost(FAILOVER_HOST) + .build(); + assertNotEquals(signed.header("Authorization"), unsigned.header("Authorization")); + } + + // ================================================================= + // withURL — URL/connect host decoupled from signing host + // ================================================================= + + // withURL overrides the connection target without changing the signature. + @Test + public void testWithURLOverridesConnectTarget() throws Exception { + URL connectUrl = new URL("https", "10.0.0.1", 8080, "/"); + Request out = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(ORIGIN_HOST) + .withURL(connectUrl) + .build(); + + // URL points to the instance IP:port. + assertEquals("10.0.0.1", out.url().host()); + assertEquals(8080, out.url().port()); + // Host header + signature are for the business domain. + assertEquals(ORIGIN_HOST, out.header("Host")); + assertNotNull(out.header("Authorization")); + } + + // The signature computed with withURL must match one computed with only + // withHost on the same signing host — i.e. withURL does not influence the + // signature, only the connection target. + @Test + public void testWithURLDoesNotAffectSignature() throws Exception { + Request noUrl = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(ORIGIN_HOST) + .build(); + Request withUrl = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .withHost(ORIGIN_HOST) + .withURL(new URL("https", "10.0.0.1", 8080, "/")) + .build(); + assertEquals(noUrl.header("Authorization"), withUrl.header("Authorization")); + assertEquals(noUrl.header("X-TC-Timestamp"), withUrl.header("X-TC-Timestamp")); + } + + // ================================================================= + // Default host fallback (no withHost) + // ================================================================= + + // When withHost is not called, the Host header / URL host are preserved + // from the original request. The request is still re-signed for that host. + @Test + public void testNoWithHostPreservesOriginHost() throws Exception { + Request out = new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .build(); + assertEquals(ORIGIN_HOST, out.url().host()); + assertEquals(ORIGIN_HOST, out.header("Host")); + assertNotNull(out.header("Authorization")); + } + + // When withHost is not called and Host header is absent, the URL host is + // used as the signing host. + @Test + public void testNoWithHostFallsBackToUrlHostWhenHeaderAbsent() throws Exception { + Request origin = new Request.Builder() + .url("https://" + ORIGIN_HOST + "/") + .header("Content-Type", "application/json") + .post(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + Request out = new RequestBuilder(origin) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_TC3_256) + .build(); + assertEquals(ORIGIN_HOST, out.header("Host")); + assertNotNull(out.header("Authorization")); + } + + // ================================================================= + // v1 signing (HmacSHA1 / HmacSHA256) + // ================================================================= + + @Test + public void testV1SignGetAppendsSignatureToQuery() throws Exception { + Request out = new RequestBuilder(newGetRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_SHA1) + .withUrlHost(FAILOVER_HOST) + .withHost(FAILOVER_HOST) + .build(); + String query = out.url().query(); + assertNotNull(query); + assertTrue(query.contains("Signature=")); + assertTrue(query.contains("SecretId=")); + assertEquals(FAILOVER_HOST, out.url().host()); + } + + @Test + public void testV1SignPostAppendsSignatureToBody() throws Exception { + Request out = new RequestBuilder(newFormPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_SHA256) + .withUrlHost(FAILOVER_HOST) + .withHost(FAILOVER_HOST) + .build(); + // Body is a form-encoded string ending with Signature=... + assertEquals(FAILOVER_HOST, out.url().host()); + assertNotNull(out.body()); + } + + @Test + public void testV1SignUnsupportedMethodThrows() throws IOException { + Request origin = new Request.Builder() + .url("https://" + ORIGIN_HOST + "/") + .header("Host", ORIGIN_HOST) + .put(RequestBody.create(MediaType.parse("application/json"), "{}".getBytes())) + .build(); + try { + new RequestBuilder(origin) + .withCredential(CRED) + .withSignMethod(ClientProfile.SIGN_SHA1) + .withHost(FAILOVER_HOST) + .build(); + fail("expected TencentCloudSDKException"); + } catch (TencentCloudSDKException e) { + assertTrue(e.getMessage().contains("Method only support")); + } + } + + // ================================================================= + // Sign skip path + // ================================================================= + + // Authorization: SKIP → buildSkip path, no signature computed. + @Test + public void testSkipV3StripsHostHeader() throws Exception { + Request out = new RequestBuilder(newSkipRequest(ORIGIN_HOST)) + .withCredential(CRED) + .withUrlHost(FAILOVER_HOST) + .withHost(FAILOVER_HOST) + .build(); + assertEquals(FAILOVER_HOST, out.header("Host")); + // No real Authorization was added — still "SKIP". + assertEquals("SKIP", out.header("Authorization")); + } + + @Test + public void testUnsupportedSignMethodThrows() throws IOException { + try { + new RequestBuilder(newPostRequest(ORIGIN_HOST, ORIGIN_HOST)) + .withCredential(CRED) + .withSignMethod("bogus") + .withHost(FAILOVER_HOST) + .build(); + fail("expected TencentCloudSDKException"); + } catch (TencentCloudSDKException e) { + assertTrue(e.getMessage().contains("invalid or not supported")); + } + } +} diff --git a/src/test/java/com/tencentcloudapi/common/http/HttpConnectionTest.java b/src/test/java/com/tencentcloudapi/common/http/HttpConnectionTest.java new file mode 100644 index 000000000..3e47f0564 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/common/http/HttpConnectionTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2018 Tencent. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.tencentcloudapi.common.http; + +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.junit.Test; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.net.Proxy; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests for {@link HttpConnection}. + * + *

Uses an OkHttp application interceptor stub to capture the {@link Request} + * that {@link HttpConnection} builds (verifying url/method/body/headers) and + * to short-circuit the call with a canned {@link Response} — no real network + * is involved. + */ +public class HttpConnectionTest { + + // ================================================================= + // Constructor / timeouts + // ================================================================= + + @Test + public void testConstructorAppliesTimeouts() { + HttpConnection conn = new HttpConnection(3, 7, 11); + OkHttpClient client = (OkHttpClient) conn.getHttpClient(); + // OkHttp exposes timeouts in milliseconds. + assertEquals(3000, client.connectTimeoutMillis()); + assertEquals(7000, client.readTimeoutMillis()); + assertEquals(11000, client.writeTimeoutMillis()); + } + + // ================================================================= + // Interceptors + // ================================================================= + + @Test + public void testAddInterceptorsPreservesOrder() { + HttpConnection conn = new HttpConnection(1, 1, 1); + Interceptor a = new NoopInterceptor(); + Interceptor b = new NoopInterceptor(); + conn.addInterceptors(a); + conn.addInterceptors(b); + List list = conn.getInterceptors(); + // Application interceptors are appended in add order. + assertTrue("first interceptor should be a", list.contains(a)); + assertTrue("second interceptor should be b", list.contains(b)); + assertEquals(a, list.get(list.size() - 2)); + assertEquals(b, list.get(list.size() - 1)); + } + + // ================================================================= + // Proxy / SSL / HostnameVerifier configuration + // ================================================================= + + @Test + public void testSetProxyAppliesToClient() { + HttpConnection conn = new HttpConnection(1, 1, 1); + Proxy proxy = new Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("127.0.0.1", 8888)); + conn.setProxy(proxy); + OkHttpClient client = (OkHttpClient) conn.getHttpClient(); + assertEquals(proxy, client.proxy()); + } + + @Test + public void testSetHostnameVerifierAppliesToClient() { + HttpConnection conn = new HttpConnection(1, 1, 1); + HostnameVerifier verifier = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + conn.setHostnameVerifier(verifier); + OkHttpClient client = (OkHttpClient) conn.getHttpClient(); + assertSame(verifier, client.hostnameVerifier()); + } + + /** + * Configuration calls chain: each {@code newBuilder().build()} carries + * forward previously set config. Verify proxy survives a subsequent + * hostname-verifier set. + */ + @Test + public void testConfigChainingPreservesPriorSettings() { + HttpConnection conn = new HttpConnection(1, 1, 1); + Proxy proxy = new Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("127.0.0.1", 8888)); + conn.setProxy(proxy); + conn.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String h, SSLSession s) { return true; } + }); + OkHttpClient client = (OkHttpClient) conn.getHttpClient(); + assertEquals("proxy must survive a later newBuilder().build()", + proxy, client.proxy()); + assertNotNull("hostnameVerifier must also be set", + client.hostnameVerifier()); + } + + // ================================================================= + // getRequest + // ================================================================= + + @Test + public void testGetRequestBuildsGetWithUrl() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + conn.getRequest("https://example.com/path?q=1"); + assertNotNull(cap.seen); + assertEquals("GET", cap.seen.method()); + assertEquals("https://example.com/path?q=1", cap.seen.url().toString()); + } + + @Test + public void testGetRequestWithHeadersCarriesHeaders() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + Headers headers = new Headers.Builder() + .add("X-TC-Action", "DescribeInstances") + .add("X-TC-Region", "ap-guangzhou") + .build(); + conn.getRequest("https://example.com/", headers); + assertEquals("DescribeInstances", cap.seen.header("X-TC-Action")); + assertEquals("ap-guangzhou", cap.seen.header("X-TC-Region")); + } + + @Test + public void testGetRequestIllegalUrlThrowsTencentCloudSdkException() { + HttpConnection conn = new HttpConnection(1, 1, 1); + try { + conn.getRequest("not a url"); + fail("expected TencentCloudSDKException for illegal url"); + } catch (TencentCloudSDKException e) { + assertTrue("message should mention IllegalArgumentException, got: " + e.getMessage(), + e.getMessage().contains("IllegalArgumentException")); + } catch (IOException e) { + fail("expected TencentCloudSDKException, got IOException: " + e); + } + } + + // ================================================================= + // postRequest + // ================================================================= + + @Test + public void testPostRequestStringBodyDefaultsFormUrlencoded() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + conn.postRequest("https://example.com/", "a=1&b=2"); + assertEquals("POST", cap.seen.method()); + assertEquals("https://example.com/", cap.seen.url().toString()); + MediaType ct = cap.seen.body().contentType(); + assertNotNull(ct); + assertEquals("application/x-www-form-urlencoded", ct.type() + "/" + ct.subtype()); + } + + @Test + public void testPostRequestStringBodyWithHeadersUsesContentType() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + Headers headers = new Headers.Builder() + .add("Content-Type", "application/json") + .add("X-TC-Action", "CallJson") + .build(); + conn.postRequest("https://example.com/", "{\"k\":\"v\"}", headers); + assertEquals("POST", cap.seen.method()); + MediaType ct = cap.seen.body().contentType(); + assertNotNull(ct); + assertEquals("application/json", ct.type() + "/" + ct.subtype()); + assertEquals("CallJson", cap.seen.header("X-TC-Action")); + } + + @Test + public void testPostRequestByteBodyWithHeaders() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + Headers headers = new Headers.Builder() + .add("Content-Type", "application/octet-stream") + .build(); + byte[] body = {1, 2, 3, 4}; + conn.postRequest("https://example.com/", body, headers); + assertEquals("POST", cap.seen.method()); + MediaType ct = cap.seen.body().contentType(); + assertNotNull(ct); + assertEquals("application/octet-stream", ct.type() + "/" + ct.subtype()); + // Body length preserved. + assertEquals(4, cap.seen.body().contentLength()); + } + + @Test + public void testPostRequestIllegalUrlThrowsTencentCloudSdkException() { + HttpConnection conn = new HttpConnection(1, 1, 1); + try { + conn.postRequest("::not a url::", "body"); + fail("expected TencentCloudSDKException for illegal url"); + } catch (TencentCloudSDKException e) { + assertTrue(e.getMessage().contains("IllegalArgumentException")); + } catch (IOException e) { + fail("expected TencentCloudSDKException, got IOException: " + e); + } + } + + // ================================================================= + // doRequest + // ================================================================= + + @Test + public void testDoRequestExecutesAndReturnsResponse() throws Exception { + CapturingInterceptor cap = new CapturingInterceptor(); + HttpConnection conn = newConnectionWith(cap); + Request req = new Request.Builder().url("https://example.com/").build(); + Response resp = conn.doRequest(req); + assertNotNull(resp); + assertEquals(200, resp.code()); + assertNotNull(cap.seen); + assertEquals("https://example.com/", cap.seen.url().toString()); + resp.close(); + } + + // ================================================================= + // setHttpClient / getHttpClient round-trip + // ================================================================= + + @Test + public void testSetGetHttpClientRoundTrip() { + HttpConnection conn = new HttpConnection(1, 1, 1); + OkHttpClient custom = new OkHttpClient(); + conn.setHttpClient(custom); + assertSame(custom, conn.getHttpClient()); + } + + // ================================================================= + // Helpers + // ================================================================= + + /** Builds an HttpConnection and inserts the capturing interceptor as the last app interceptor. */ + private static HttpConnection newConnectionWith(CapturingInterceptor cap) { + HttpConnection conn = new HttpConnection(1, 1, 1); + conn.addInterceptors(cap); + return conn; + } + + /** A no-op interceptor that just proceeds. */ + private static final class NoopInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request()); + } + } + + /** + * Captures the request handed to {@code chain.proceed} and returns a canned + * 200 response, short-circuiting the network. + */ + private static final class CapturingInterceptor implements Interceptor { + Request seen; + + @Override + public Response intercept(Chain chain) throws IOException { + seen = chain.request(); + return new Response.Builder() + .request(seen) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(null, "")) + .build(); + } + } +} diff --git a/src/test/java/com/tencentcloudapi/integration/common/OIDCRoleProviderTest.java b/src/test/java/com/tencentcloudapi/integration/common/OIDCRoleProviderTest.java new file mode 100644 index 000000000..067f9234d --- /dev/null +++ b/src/test/java/com/tencentcloudapi/integration/common/OIDCRoleProviderTest.java @@ -0,0 +1,133 @@ +package com.tencentcloudapi.integration.common; + +import com.tencentcloudapi.common.http.HttpConnection; +import com.tencentcloudapi.common.provider.OIDCRoleArnProvider; +import okhttp3.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + + +public class OIDCRoleProviderTest { + + private final MyInterceptor interceptor = new MyInterceptor(); + private Object oldClient; + + @Before + public void setUp() throws Exception { + OkHttpClient okClient = new OkHttpClient.Builder() + .addInterceptor(interceptor) + .build(); + + Field field = HttpConnection.class.getDeclaredField("clientSingleton"); + field.setAccessible(true); + + oldClient = field.get(null); + putStatic(field, okClient); + } + + @After + public void teardown() throws Exception { + Field field = HttpConnection.class.getDeclaredField("clientSingleton"); + field.setAccessible(true); + putStatic(field, oldClient); + } + + /** + * * Writes a value to a {@code private static final} field, working around the {@code final} + * * restriction on Java 9+ via {@code sun.misc.Unsafe.putObject} on the field's base+offset. + * */ + private static void putStatic(Field field, Object value) throws Exception { + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Object unsafe = unsafeField.get(null); + + Method objectFieldOffset = unsafeClass.getMethod("staticFieldOffset", Field.class); + Method staticFieldBase = unsafeClass.getMethod("staticFieldBase", Field.class); + Method putObject = unsafeClass.getMethod("putObject", Object.class, long.class, Object.class); + + Object base = staticFieldBase.invoke(unsafe, field); + long offset = (Long) objectFieldOffset.invoke(unsafe, field); + putObject.invoke(unsafe, base, offset, value); + } + + static class MyInterceptor implements Interceptor { + + private String realHost; + + public String getRealHost() { + return this.realHost; + } + + + @Override + public Response intercept(Chain chain) { + Request request = chain.request(); + + realHost = request.url().host(); + + String mockResponseJson = "{\"Response\": {" + + "\"Credentials\": {" + + "\"Token\":\"mock-oidc-token\"," + + "\"TmpSecretId\":\"mock-oidc-tmp-secret-id\"," + + "\"TmpSecretKey\":\"mock-oidc-tmp-secret-key\"" + + "}," + + "\"ExpiredTime\":" + (System.currentTimeMillis() / 1000 + 7200) + "," + + "\"Expiration\":\"2025-12-31T23:59:59Z\"," + + "\"RequestId\":\"mock-oidc-request-id\"}}"; + return new Response.Builder() + .request(request) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(okhttp3.ResponseBody.create( + MediaType.parse("application/json"), + mockResponseJson + )) + .build(); + } + } + + @Test + public void testOIDCRoleProviderWithDefaultEndpoint() throws Exception { + String expectedHost = "sts.tencentcloudapi.com"; + + OIDCRoleArnProvider cred = new OIDCRoleArnProvider( + "ap-guangzhou", + "test-provider-id", + "test-web-identity-token", + "test-role-arn", + "test-role-session-name", + 7200 + ); + + cred.getCredentials(); + + Assert.assertEquals(expectedHost, interceptor.getRealHost()); + } + + @Test + public void testOIDCRoleProviderWithSetEndpoint() throws Exception { + String expectedHost = "sts.internal.tencentcloudapi.com"; + + OIDCRoleArnProvider cred = new OIDCRoleArnProvider( + "ap-guangzhou", + "test-provider-id", + "test-web-identity-token", + "test-role-arn", + "test-role-session-name", + 7200, + expectedHost + ); + + cred.getCredentials(); + + Assert.assertEquals(expectedHost, interceptor.getRealHost()); + } +} + diff --git a/src/test/java/com/tencentcloudapi/integration/common/STSCredentialTest.java b/src/test/java/com/tencentcloudapi/integration/common/STSCredentialTest.java new file mode 100644 index 000000000..c70a515d4 --- /dev/null +++ b/src/test/java/com/tencentcloudapi/integration/common/STSCredentialTest.java @@ -0,0 +1,112 @@ +package com.tencentcloudapi.integration.common; + +import com.tencentcloudapi.common.http.HttpConnection; +import com.tencentcloudapi.common.provider.STSCredential; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + + +public class STSCredentialTest { + + private final OIDCRoleProviderTest.MyInterceptor interceptor = new OIDCRoleProviderTest.MyInterceptor(); + private Object oldClient; + + @Before + public void setUp() throws Exception { + OkHttpClient okClient = new OkHttpClient.Builder() + .addInterceptor(interceptor) + .build(); + + Field field = HttpConnection.class.getDeclaredField("clientSingleton"); + field.setAccessible(true); + + oldClient = field.get(null); + putStatic(field, okClient); + } + + @After + public void teardown() throws Exception { + Field field = HttpConnection.class.getDeclaredField("clientSingleton"); + field.setAccessible(true); + putStatic(field, oldClient); + } + + /** + * * Writes a value to a {@code private static final} field, working around the {@code final} + * * restriction on Java 9+ via {@code sun.misc.Unsafe.putObject} on the field's base+offset. + * */ + private static void putStatic(Field field, Object value) throws Exception { + Class unsafeClass = Class.forName("sun.misc.Unsafe"); + Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Object unsafe = unsafeField.get(null); + + Method objectFieldOffset = unsafeClass.getMethod("staticFieldOffset", Field.class); + Method staticFieldBase = unsafeClass.getMethod("staticFieldBase", Field.class); + Method putObject = unsafeClass.getMethod("putObject", Object.class, long.class, Object.class); + + Object base = staticFieldBase.invoke(unsafe, field); + long offset = (Long) objectFieldOffset.invoke(unsafe, field); + putObject.invoke(unsafe, base, offset, value); + } + + static class MyInterceptor implements Interceptor { + + private String realHost; + + public String getRealHost() { + return this.realHost; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + this.realHost = request.url().host(); + return chain.proceed(request); + } + } + + @Test + public void testSTSCredentialWithDefaultEndpoint() throws Exception { + String expectedHost = "sts.tencentcloudapi.com"; + + STSCredential cred = new STSCredential( + "test-secret-id", + "test-secret-key", + "test-role-arn", + "test-role-session-name" + ); + + cred.getToken(); + + Assert.assertEquals(expectedHost, interceptor.getRealHost()); + } + + @Test + public void testSTSCredentialWithSetEndpoint() throws Exception { + String expectedHost = "sts.internal.tencentcloudapi.com"; + + STSCredential cred = new STSCredential( + "test-secret-id", + "test-secret-key", + "test-role-arn", + "test-role-session-name", + expectedHost + ); + + cred.getToken(); + + Assert.assertEquals(expectedHost, interceptor.getRealHost()); + } +} + diff --git a/src/test/java/com/tencentcloudapi/integration/common/provider/ProfileCredentialsProviderTest.java b/src/test/java/com/tencentcloudapi/integration/common/provider/ProfileCredentialsProviderTest.java new file mode 100644 index 000000000..3b6a7524a --- /dev/null +++ b/src/test/java/com/tencentcloudapi/integration/common/provider/ProfileCredentialsProviderTest.java @@ -0,0 +1,54 @@ +package com.tencentcloudapi.integration.common.provider; + +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.provider.ProfileCredentialsProvider; +import org.junit.Test; +import java.nio.file.Path; +import java.nio.file.Files; + +import static org.junit.Assert.*; + +public class ProfileCredentialsProviderTest { + + @Test + public void testGetCredentials() throws Exception { + // 创建临时目录模拟用户主目录 + Path tempHomeDir = Files.createTempDirectory("tencentcloud-test-home"); + Path credentialsDir = tempHomeDir.resolve(".tencentcloud"); + Files.createDirectories(credentialsDir); + Path credentialsFile = credentialsDir.resolve("credentials"); + + // 保存原始user.home属性 + String originalUserHome = System.getProperty("user.home"); + + try { + // 设置临时目录为用户主目录 + System.setProperty("user.home", tempHomeDir.toString()); + + // 写入配置文件内容 + String configContent = "[default]\n" + + "secret_id = secret_id_test\n" + + "secret_key = secret_key_test"; + Files.write(credentialsFile, configContent.getBytes()); + + // 测试ProfileCredentialsProvider是否能正确读取 + ProfileCredentialsProvider provider = new ProfileCredentialsProvider(); + Credential cred = provider.getCredentials().getSnapshot(); + + // 验证读取的凭据是否正确 + assertEquals("secret_id_test", cred.getSecretId()); + assertEquals("secret_key_test", cred.getSecretKey()); + + } finally { + // 恢复原始user.home属性 + if (originalUserHome != null) { + System.setProperty("user.home", originalUserHome); + } + + // 清理临时文件 + Files.deleteIfExists(credentialsFile); + Files.deleteIfExists(credentialsDir); + Files.deleteIfExists(tempHomeDir); + } + } +} diff --git a/src/test/java/com/tencentcloudapi/integration/requests/DataTypeTest.java b/src/test/java/com/tencentcloudapi/integration/requests/DataTypeTest.java index ac8ea2fb0..cd81ea70f 100644 --- a/src/test/java/com/tencentcloudapi/integration/requests/DataTypeTest.java +++ b/src/test/java/com/tencentcloudapi/integration/requests/DataTypeTest.java @@ -6,8 +6,13 @@ import com.tencentcloudapi.cbs.v20170312.models.ModifySnapshotAttributeRequest; import com.tencentcloudapi.common.Credential; import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.Language; import com.tencentcloudapi.cvm.v20170312.CvmClient; import com.tencentcloudapi.cvm.v20170312.models.DescribeInstancesRequest; +import com.tencentcloudapi.faceid.v20180301.FaceidClient; +import com.tencentcloudapi.faceid.v20180301.models.Encryption; +import com.tencentcloudapi.faceid.v20180301.models.MobileStatusRequest; import com.tencentcloudapi.iai.v20200303.IaiClient; import com.tencentcloudapi.iai.v20200303.models.SearchFacesRequest; import org.junit.Test; @@ -99,4 +104,33 @@ public void TestFloatType() { } } } + + @Test + public void TestComplexType() { + Credential cred = new Credential( + System.getenv("TENCENTCLOUD_SECRET_ID"), + System.getenv("TENCENTCLOUD_SECRET_KEY") + ); + + ClientProfile cpf = new ClientProfile(); + cpf.setLanguage(Language.EN_US); + + FaceidClient client = new FaceidClient(cred, "ap-guangzhou"); + MobileStatusRequest req = new MobileStatusRequest(); + req.setMobile("null"); + Encryption encryption = new Encryption(); + encryption.setCiphertextBlob("null"); + encryption.setEncryptList(new String[]{"null", "null"}); + encryption.setIv("null"); + req.setEncryption(encryption); + + try { + client.MobileStatus(req); + throw new RuntimeException("unexpected success"); + } catch (TencentCloudSDKException e) { + if (!e.getErrorCode().contains("UnauthorizedOperation.Nonactivated")) { + throw new RuntimeException("unexpected error", e); + } + } + } } diff --git a/src/test/java/com/tencentcloudapi/integration/requests/TempCredentialTest.java b/src/test/java/com/tencentcloudapi/integration/requests/TempCredentialTest.java new file mode 100644 index 000000000..2154c66fb --- /dev/null +++ b/src/test/java/com/tencentcloudapi/integration/requests/TempCredentialTest.java @@ -0,0 +1,40 @@ +package com.tencentcloudapi.integration.requests; + +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.cvm.v20170312.CvmClient; +import com.tencentcloudapi.cvm.v20170312.models.DescribeInstancesRequest; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +public class TempCredentialTest { + @Test + public void TestEmptyToken() throws TencentCloudSDKException { + List reqMethods = Arrays.asList("GET", "POST"); + List signMethods = Arrays.asList( + ClientProfile.SIGN_SHA1, ClientProfile.SIGN_SHA256, ClientProfile.SIGN_TC3_256); + List tokens = Arrays.asList("", null); + + for (String reqMethod : reqMethods) { + for (String signMethod : signMethods) { + for (String token : tokens) { + Credential cred = new Credential( + System.getenv("TENCENTCLOUD_SECRET_ID"), + System.getenv("TENCENTCLOUD_SECRET_KEY"), + token + ); + + ClientProfile cpf = new ClientProfile(); + cpf.getHttpProfile().setReqMethod(reqMethod); + cpf.setSignMethod(signMethod); + CvmClient client = new CvmClient(cred, "ap-guangzhou", cpf); + DescribeInstancesRequest req = new DescribeInstancesRequest(); + client.DescribeInstances(req); + } + } + } + } +}