CommonCallback.java

package io.featureprobe.api.hook;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.featureprobe.api.base.exception.SignatureException;
import io.featureprobe.api.base.hook.ICallback;
import io.featureprobe.api.base.model.CallbackResult;
import io.featureprobe.api.base.model.HookContext;
import io.featureprobe.api.mapper.HookContextMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component(value = "COMMON")
public class CommonCallback implements ICallback {

    private static final String USER_AGENT_KEY = "User-Agent";
    private static final String USER_AGENT_VALUE = "FeatureProbe-Webhook/1.0";

    private static final String SIGN_KEY = "X-FeatureProbe-Sign";


    private final OkHttpClient httpClient = new OkHttpClient.Builder()
            .connectionPool( new ConnectionPool(5, 5, TimeUnit.SECONDS))
            .connectTimeout(Duration.ofSeconds(3))
            .readTimeout(Duration.ofSeconds(3))
            .writeTimeout(Duration.ofSeconds(3))
            .retryOnConnectionFailure(true)
            .build();

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public CallbackResult callback(HookContext hookContext, String url, String secretKey) {
        CallbackResult result = new CallbackResult();
        try {
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
            String requestBodyStr = mapper.writeValueAsString(HookContextMapper.INSTANCE
                    .contextToRequestBody(hookContext));
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), requestBodyStr);
            Request request = new Request.Builder()
                    .header(USER_AGENT_KEY, USER_AGENT_VALUE)
                    .header(SIGN_KEY, sign(secretKey, requestBodyStr))
                    .url(url)
                    .post(requestBody)
                    .build();
            Response response = httpClient.newCall(request).execute();
            log.debug("Common Callback response: {}", response);
            result.setSuccess(response.isSuccessful());
            result.setRequestBody(requestBodyStr);
            result.setStatusCode(response.code());
            result.setResponseBody(response.body().string());
        } catch (Exception e) {
            log.error("Common Callback error", e);
            result.setSuccess(false);
            result.setErrorMessage(e.getMessage());
            result.setTime(new Date());
            return result;
        }
        result.setTime(new Date());
        return result;
    }

    private String sign(String secretKey, String content) {

        try {
            SecretKeySpec signinKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
            Mac mac = Mac.getInstance("HmacSHA1");
            mac.init(signinKey);
            byte[] rawHmac = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(rawHmac);
        } catch (Exception e) {
            log.error("WebHook Callback failed sign for key:{} and content:{}", secretKey, content, e);
            throw new SignatureException(e.getMessage());
        }
    }

}