package cc.blynk.server.core.processors;
import cc.blynk.server.core.model.DashBoard;
import cc.blynk.server.core.model.Pin;
import cc.blynk.server.core.model.auth.Session;
import cc.blynk.server.core.model.enums.PinType;
import cc.blynk.server.core.model.widgets.others.webhook.Header;
import cc.blynk.server.core.model.widgets.others.webhook.SupportedWebhookMethod;
import cc.blynk.server.core.model.widgets.others.webhook.WebHook;
import cc.blynk.server.core.protocol.enums.Command;
import cc.blynk.server.core.protocol.exceptions.QuotaLimitException;
import cc.blynk.server.core.stats.GlobalStats;
import cc.blynk.utils.StringUtils;
import io.netty.util.CharsetUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.asynchttpclient.*;
import java.time.Instant;
import static cc.blynk.server.core.protocol.enums.Command.WEB_HOOKS;
import static cc.blynk.utils.StringUtils.*;
/**
* Handles all webhooks logic.
*
* The Blynk Project.
* Created by Dmitriy Dumanskiy.
* Created on 05.09.16.
*/
public class WebhookProcessor extends NotificationBase {
private static final Logger log = LogManager.getLogger(WebhookProcessor.class);
private final AsyncHttpClient httpclient;
private final GlobalStats globalStats;
private final int responseSizeLimit;
private final String email;
private final int WEBHOOK_FAILURE_LIMIT;
public WebhookProcessor(DefaultAsyncHttpClient httpclient,
long quotaFrequencyLimit,
int responseSizeLimit,
int failureLimit,
GlobalStats stats, String email) {
super(quotaFrequencyLimit);
this.httpclient = httpclient;
this.globalStats = stats;
this.responseSizeLimit = responseSizeLimit;
this.email = email;
this.WEBHOOK_FAILURE_LIMIT = failureLimit;
}
public void process(Session session, DashBoard dash, int deviceId, byte pin, PinType pinType, String triggerValue, long now) {
WebHook widget = dash.findWebhookByPin(deviceId, pin, pinType);
if (widget == null) {
return;
}
try {
checkIfNotificationQuotaLimitIsNotReached(now);
} catch (QuotaLimitException qle) {
log.debug("Webhook quota limit reached. Ignoring hook.");
return;
}
process(session, dash.id, deviceId, widget, triggerValue);
}
private void process(Session session, int dashId, int deviceId, WebHook webHook, String triggerValue) {
if (!webHook.isValid(WEBHOOK_FAILURE_LIMIT)) {
return;
}
String newUrl = format(webHook.url, triggerValue, false);
BoundRequestBuilder builder = buildRequestMethod(webHook.method, newUrl);
if (webHook.headers != null) {
for (Header header : webHook.headers) {
if (header.isValid()) {
builder.setHeader(header.name, header.value);
if (webHook.body != null && !webHook.body.isEmpty()) {
if (header.name.equals("Content-Type")) {
String newBody = format(webHook.body, triggerValue, true);
log.trace("Webhook formatted body : {}", newBody);
buildRequestBody(builder, header.value, newBody);
}
}
}
}
}
log.trace("Sending webhook. ", webHook);
builder.execute(new AsyncCompletionHandler<Response>() {
private int length = 0;
@Override
public State onBodyPartReceived(HttpResponseBodyPart content) throws Exception {
length += content.length();
if (length > responseSizeLimit) {
log.warn("Response from webhook is too big for {}. Skipping. Size : {}", email, length);
return State.ABORT;
}
return super.onBodyPartReceived(content);
}
@Override
public Response onCompleted(Response response) throws Exception {
if (response.getStatusCode() == 200 || response.getStatusCode() == 302) {
webHook.failureCounter = 0;
if (response.hasResponseBody()) {
//todo could be optimized
String body = Pin.makeHardwareBody(webHook.pinType, webHook.pin, response.getResponseBody(CharsetUtil.UTF_8));
log.trace("Sending webhook to hardware. {}", body);
session.sendMessageToHardware(dashId, Command.HARDWARE, 888, body, deviceId);
}
} else {
webHook.failureCounter++;
log.error("Error sending webhook for {}. Code {}.", email, response.getStatusCode());
if (log.isDebugEnabled()) {
log.debug("Reason {}", response.getResponseBody());
}
}
return null;
}
@Override
public void onThrowable(Throwable t) {
webHook.failureCounter++;
log.error("Error sending webhook for {}.", email);
if (log.isDebugEnabled()) {
log.debug("Reason {}", t.getMessage());
}
}
});
globalStats.mark(WEB_HOOKS);
}
//todo this is very straightforward solution. should be optimized.
private String format(String data, String triggerValue, boolean doBlynkCheck) {
//this is an ugly hack to make it work with Blynk HTTP API.
if (doBlynkCheck || !data.toLowerCase().contains("/pin/v")) {
data = PIN_PATTERN.matcher(data).replaceFirst(triggerValue);
}
data = data.replace("%s", triggerValue);
String[] splitted = triggerValue.split(StringUtils.BODY_SEPARATOR_STRING);
switch (splitted.length) {
case 6 :
data = PIN_PATTERN_5.matcher(data).replaceFirst(splitted[5]);
case 5 :
data = PIN_PATTERN_4.matcher(data).replaceFirst(splitted[4]);
case 4 :
data = PIN_PATTERN_3.matcher(data).replaceFirst(splitted[3]);
case 3 :
data = PIN_PATTERN_2.matcher(data).replaceFirst(splitted[2]);
case 2 :
data = PIN_PATTERN_1.matcher(data).replaceFirst(splitted[1]);
case 1 :
data = PIN_PATTERN_0.matcher(data).replaceFirst(splitted[0]);
}
data = DATETIME_PATTERN.matcher(data).replaceFirst(Instant.now().toString());
return data;
}
private void buildRequestBody(BoundRequestBuilder builder, String header, String body) {
switch (header) {
case "application/json" :
case "text/plain" :
builder.setBody(body);
break;
default :
throw new IllegalArgumentException("Unsupported content-type for webhook.");
}
}
private BoundRequestBuilder buildRequestMethod(SupportedWebhookMethod method, String url) {
switch (method) {
case GET :
return httpclient.prepareGet(url);
case POST :
return httpclient.preparePost(url);
case PUT :
return httpclient.preparePut(url);
case DELETE :
return httpclient.prepareDelete(url);
default :
throw new IllegalArgumentException("Unsupported method type for webhook.");
}
}
}