From fb5e953f90f0bf48593feb88138a5ee1706b6989 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Fri, 2 Nov 2018 16:05:31 +1300 Subject: Implement universal HTTP SMS manager --- src/org/traccar/Context.java | 3 +- src/org/traccar/notificators/NotificatorSms.java | 1 + src/org/traccar/smpp/ClientSmppSessionHandler.java | 82 ------- src/org/traccar/smpp/EnquireLinkTask.java | 59 ----- src/org/traccar/smpp/ReconnectionTask.java | 31 --- src/org/traccar/smpp/SmppClient.java | 271 -------------------- src/org/traccar/sms/HttpSmsClient.java | 90 +++++++ src/org/traccar/sms/SmsGatewayClient.java | 71 ------ src/org/traccar/sms/SmsManager.java | 8 +- .../traccar/sms/smpp/ClientSmppSessionHandler.java | 83 +++++++ src/org/traccar/sms/smpp/EnquireLinkTask.java | 59 +++++ src/org/traccar/sms/smpp/ReconnectionTask.java | 32 +++ src/org/traccar/sms/smpp/SmppClient.java | 272 +++++++++++++++++++++ 13 files changed, 544 insertions(+), 518 deletions(-) delete mode 100644 src/org/traccar/smpp/ClientSmppSessionHandler.java delete mode 100644 src/org/traccar/smpp/EnquireLinkTask.java delete mode 100644 src/org/traccar/smpp/ReconnectionTask.java delete mode 100644 src/org/traccar/smpp/SmppClient.java create mode 100644 src/org/traccar/sms/HttpSmsClient.java delete mode 100644 src/org/traccar/sms/SmsGatewayClient.java create mode 100644 src/org/traccar/sms/smpp/ClientSmppSessionHandler.java create mode 100644 src/org/traccar/sms/smpp/EnquireLinkTask.java create mode 100644 src/org/traccar/sms/smpp/ReconnectionTask.java create mode 100644 src/org/traccar/sms/smpp/SmppClient.java diff --git a/src/org/traccar/Context.java b/src/org/traccar/Context.java index 5b22120d7..3c3dac874 100644 --- a/src/org/traccar/Context.java +++ b/src/org/traccar/Context.java @@ -85,6 +85,7 @@ import org.traccar.notification.JsonTypeEventForwarder; import org.traccar.notification.NotificatorManager; import org.traccar.reports.model.TripsConfig; import org.traccar.sms.SmsManager; +import org.traccar.sms.smpp.SmppClient; import org.traccar.web.WebServer; import javax.ws.rs.client.Client; @@ -422,7 +423,7 @@ public final class Context { tripsConfig = initTripsConfig(); if (config.getBoolean("sms.enable")) { - final String smsManagerClass = config.getString("sms.manager.class", "org.traccar.smpp.SmppClient"); + final String smsManagerClass = config.getString("sms.manager.class", SmppClient.class.getCanonicalName()); try { smsManager = (SmsManager) Class.forName(smsManagerClass).newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { diff --git a/src/org/traccar/notificators/NotificatorSms.java b/src/org/traccar/notificators/NotificatorSms.java index cc19f9a58..d4052c626 100644 --- a/src/org/traccar/notificators/NotificatorSms.java +++ b/src/org/traccar/notificators/NotificatorSms.java @@ -56,4 +56,5 @@ public final class NotificatorSms extends Notificator { NotificationFormatter.formatShortMessage(userId, event, position), false); } } + } diff --git a/src/org/traccar/smpp/ClientSmppSessionHandler.java b/src/org/traccar/smpp/ClientSmppSessionHandler.java deleted file mode 100644 index 4c5522c14..000000000 --- a/src/org/traccar/smpp/ClientSmppSessionHandler.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2017 Anton Tananaev (anton@traccar.org) - * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) - * - * 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 org.traccar.smpp; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.traccar.events.TextMessageEventHandler; - -import com.cloudhopper.commons.charset.CharsetUtil; -import com.cloudhopper.smpp.SmppConstants; -import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; -import com.cloudhopper.smpp.pdu.DeliverSm; -import com.cloudhopper.smpp.pdu.PduRequest; -import com.cloudhopper.smpp.pdu.PduResponse; -import com.cloudhopper.smpp.util.SmppUtil; - -public class ClientSmppSessionHandler extends DefaultSmppSessionHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(ClientSmppSessionHandler.class); - - private SmppClient smppClient; - - public ClientSmppSessionHandler(SmppClient smppClient) { - this.smppClient = smppClient; - } - - @Override - public void firePduRequestExpired(PduRequest pduRequest) { - LOGGER.warn("PDU request expired: " + pduRequest); - } - - @Override - public PduResponse firePduRequestReceived(PduRequest request) { - PduResponse response; - try { - if (request instanceof DeliverSm) { - String sourceAddress = ((DeliverSm) request).getSourceAddress().getAddress(); - String message = CharsetUtil.decode(((DeliverSm) request).getShortMessage(), - smppClient.mapDataCodingToCharset(((DeliverSm) request).getDataCoding())); - LOGGER.info("SMS Message Received: " + message.trim() + ", Source Address: " + sourceAddress); - - boolean isDeliveryReceipt; - if (smppClient.getDetectDlrByOpts()) { - isDeliveryReceipt = request.getOptionalParameters() != null; - } else { - isDeliveryReceipt = SmppUtil.isMessageTypeAnyDeliveryReceipt(((DeliverSm) request).getEsmClass()); - } - - if (!isDeliveryReceipt) { - TextMessageEventHandler.handleTextMessage(sourceAddress, message); - } - } - response = request.createResponse(); - } catch (Exception error) { - LOGGER.warn("SMS receiving error", error); - response = request.createResponse(); - response.setResultMessage(error.getMessage()); - response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR); - } - return response; - } - - @Override - public void fireChannelUnexpectedlyClosed() { - LOGGER.warn("SMPP session channel unexpectedly closed"); - smppClient.scheduleReconnect(); - } -} diff --git a/src/org/traccar/smpp/EnquireLinkTask.java b/src/org/traccar/smpp/EnquireLinkTask.java deleted file mode 100644 index 3072f975a..000000000 --- a/src/org/traccar/smpp/EnquireLinkTask.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 Anton Tananaev (anton@traccar.org) - * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) - * - * 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 org.traccar.smpp; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.cloudhopper.smpp.SmppSession; -import com.cloudhopper.smpp.pdu.EnquireLink; -import com.cloudhopper.smpp.type.RecoverablePduException; -import com.cloudhopper.smpp.type.SmppChannelException; -import com.cloudhopper.smpp.type.SmppTimeoutException; -import com.cloudhopper.smpp.type.UnrecoverablePduException; - -public class EnquireLinkTask implements Runnable { - - private static final Logger LOGGER = LoggerFactory.getLogger(EnquireLinkTask.class); - - private SmppClient smppClient; - private Integer enquireLinkTimeout; - - public EnquireLinkTask(SmppClient smppClient, Integer enquireLinkTimeout) { - this.smppClient = smppClient; - this.enquireLinkTimeout = enquireLinkTimeout; - } - - @Override - public void run() { - SmppSession smppSession = smppClient.getSession(); - if (smppSession != null && smppSession.isBound()) { - try { - smppSession.enquireLink(new EnquireLink(), enquireLinkTimeout); - } catch (SmppTimeoutException | SmppChannelException - | RecoverablePduException | UnrecoverablePduException error) { - LOGGER.warn("Enquire link failed, executing reconnect: ", error); - smppClient.scheduleReconnect(); - } catch (InterruptedException error) { - LOGGER.info("Enquire link interrupted, probably killed by reconnecting"); - } - } else { - LOGGER.warn("Enquire link running while session is not connected"); - } - } - -} diff --git a/src/org/traccar/smpp/ReconnectionTask.java b/src/org/traccar/smpp/ReconnectionTask.java deleted file mode 100644 index c9d9173ae..000000000 --- a/src/org/traccar/smpp/ReconnectionTask.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Anton Tananaev (anton@traccar.org) - * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) - * - * 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 org.traccar.smpp; - -public class ReconnectionTask implements Runnable { - - private final SmppClient smppClient; - - protected ReconnectionTask(SmppClient smppClient) { - this.smppClient = smppClient; - } - - @Override - public void run() { - smppClient.reconnect(); - } -} diff --git a/src/org/traccar/smpp/SmppClient.java b/src/org/traccar/smpp/SmppClient.java deleted file mode 100644 index d4f201871..000000000 --- a/src/org/traccar/smpp/SmppClient.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org) - * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org) - * - * 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 org.traccar.smpp; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.traccar.Context; -import org.traccar.notification.MessageException; -import org.traccar.sms.SmsManager; - -import com.cloudhopper.commons.charset.CharsetUtil; -import com.cloudhopper.smpp.SmppBindType; -import com.cloudhopper.smpp.SmppConstants; -import com.cloudhopper.smpp.SmppSession; -import com.cloudhopper.smpp.SmppSessionConfiguration; -import com.cloudhopper.smpp.impl.DefaultSmppClient; -import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; -import com.cloudhopper.smpp.pdu.SubmitSm; -import com.cloudhopper.smpp.pdu.SubmitSmResp; -import com.cloudhopper.smpp.tlv.Tlv; -import com.cloudhopper.smpp.type.Address; -import com.cloudhopper.smpp.type.RecoverablePduException; -import com.cloudhopper.smpp.type.SmppChannelException; -import com.cloudhopper.smpp.type.SmppTimeoutException; -import com.cloudhopper.smpp.type.UnrecoverablePduException; - -public class SmppClient implements SmsManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(SmppClient.class); - - private SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration(); - private SmppSession smppSession; - private DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler(this); - private ExecutorService executorService = Executors.newCachedThreadPool(); - private DefaultSmppClient clientBootstrap = new DefaultSmppClient(); - - private ScheduledExecutorService enquireLinkExecutor; - private ScheduledFuture enquireLinkTask; - private Integer enquireLinkPeriod; - private Integer enquireLinkTimeout; - - private ScheduledExecutorService reconnectionExecutor; - private ScheduledFuture reconnectionTask; - private Integer reconnectionDelay; - - private String sourceAddress; - private String commandSourceAddress; - private int submitTimeout; - private boolean requestDlr; - private boolean detectDlrByOpts; - private String notificationsCharsetName; - private byte notificationsDataCoding; - private String commandsCharsetName; - private byte commandsDataCoding; - - private byte sourceTon; - private byte sourceNpi; - private byte commandSourceTon; - private byte commandSourceNpi; - - private byte destTon; - private byte destNpi; - - public SmppClient() { - sessionConfig.setName("Traccar.smppSession"); - sessionConfig.setInterfaceVersion( - (byte) Context.getConfig().getInteger("sms.smpp.version", SmppConstants.VERSION_3_4)); - sessionConfig.setType(SmppBindType.TRANSCEIVER); - sessionConfig.setHost(Context.getConfig().getString("sms.smpp.host", "localhost")); - sessionConfig.setPort(Context.getConfig().getInteger("sms.smpp.port", 2775)); - sessionConfig.setSystemId(Context.getConfig().getString("sms.smpp.username", "user")); - sessionConfig.setSystemType(Context.getConfig().getString("sms.smpp.systemType", null)); - sessionConfig.setPassword(Context.getConfig().getString("sms.smpp.password", "password")); - sessionConfig.getLoggingOptions().setLogBytes(false); - sessionConfig.getLoggingOptions().setLogPdu(Context.getConfig().getBoolean("sms.smpp.logPdu")); - - sourceAddress = Context.getConfig().getString("sms.smpp.sourceAddress", ""); - commandSourceAddress = Context.getConfig().getString("sms.smpp.commandSourceAddress", sourceAddress); - submitTimeout = Context.getConfig().getInteger("sms.smpp.submitTimeout", 10000); - - requestDlr = Context.getConfig().getBoolean("sms.smpp.requestDlr"); - detectDlrByOpts = Context.getConfig().getBoolean("sms.smpp.detectDlrByOpts"); - - notificationsCharsetName = Context.getConfig().getString("sms.smpp.notificationsCharset", - CharsetUtil.NAME_UCS_2); - notificationsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.notificationsDataCoding", - SmppConstants.DATA_CODING_UCS2); - commandsCharsetName = Context.getConfig().getString("sms.smpp.commandsCharset", - CharsetUtil.NAME_GSM); - commandsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.commandsDataCoding", - SmppConstants.DATA_CODING_DEFAULT); - - - sourceTon = (byte) Context.getConfig().getInteger("sms.smpp.sourceTon", SmppConstants.TON_ALPHANUMERIC); - commandSourceTon = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceTon", sourceTon); - sourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.sourceNpi", SmppConstants.NPI_UNKNOWN); - commandSourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceNpi", sourceNpi); - - destTon = (byte) Context.getConfig().getInteger("sms.smpp.destTon", SmppConstants.TON_INTERNATIONAL); - destNpi = (byte) Context.getConfig().getInteger("sms.smpp.destNpi", SmppConstants.NPI_E164); - - enquireLinkPeriod = Context.getConfig().getInteger("sms.smpp.enquireLinkPeriod", 60000); - enquireLinkTimeout = Context.getConfig().getInteger("sms.smpp.enquireLinkTimeout", 10000); - enquireLinkExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - String name = sessionConfig.getName(); - thread.setName("EnquireLink-" + name); - return thread; - } - }); - - reconnectionDelay = Context.getConfig().getInteger("sms.smpp.reconnectionDelay", 10000); - reconnectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable); - String name = sessionConfig.getName(); - thread.setName("Reconnection-" + name); - return thread; - } - }); - - scheduleReconnect(); - } - - public synchronized SmppSession getSession() { - return smppSession; - } - - public String mapDataCodingToCharset(byte dataCoding) { - switch (dataCoding) { - case SmppConstants.DATA_CODING_LATIN1: - return CharsetUtil.NAME_ISO_8859_1; - case SmppConstants.DATA_CODING_UCS2: - return CharsetUtil.NAME_UCS_2; - default: - return CharsetUtil.NAME_GSM; - } - } - - public boolean getDetectDlrByOpts() { - return detectDlrByOpts; - } - - protected synchronized void reconnect() { - try { - disconnect(); - smppSession = clientBootstrap.bind(sessionConfig, sessionHandler); - stopReconnectionkTask(); - runEnquireLinkTask(); - LOGGER.info("SMPP session connected"); - } catch (SmppTimeoutException | SmppChannelException - | UnrecoverablePduException | InterruptedException error) { - LOGGER.warn("Unable to connect to SMPP server: ", error); - } - } - - public void scheduleReconnect() { - if (reconnectionTask == null || reconnectionTask.isDone()) { - reconnectionTask = reconnectionExecutor.scheduleWithFixedDelay( - new ReconnectionTask(this), - reconnectionDelay, reconnectionDelay, TimeUnit.MILLISECONDS); - } - } - - private void stopReconnectionkTask() { - if (reconnectionTask != null) { - reconnectionTask.cancel(false); - } - } - - private void disconnect() { - stopEnquireLinkTask(); - destroySession(); - } - - private void runEnquireLinkTask() { - enquireLinkTask = enquireLinkExecutor.scheduleWithFixedDelay( - new EnquireLinkTask(this, enquireLinkTimeout), - enquireLinkPeriod, enquireLinkPeriod, TimeUnit.MILLISECONDS); - } - - private void stopEnquireLinkTask() { - if (enquireLinkTask != null) { - enquireLinkTask.cancel(true); - } - } - - private void destroySession() { - if (smppSession != null) { - LOGGER.info("Cleaning up SMPP session... "); - smppSession.destroy(); - smppSession = null; - } - } - - @Override - public synchronized void sendMessageSync(String destAddress, String message, boolean command) - throws MessageException, InterruptedException, IllegalStateException { - if (getSession() != null && getSession().isBound()) { - try { - SubmitSm submit = new SubmitSm(); - byte[] textBytes; - textBytes = CharsetUtil.encode(message, command ? commandsCharsetName : notificationsCharsetName); - submit.setDataCoding(command ? commandsDataCoding : notificationsDataCoding); - if (requestDlr) { - submit.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED); - } - - if (textBytes != null && textBytes.length > 255) { - submit.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, textBytes, - "message_payload")); - } else { - submit.setShortMessage(textBytes); - } - - submit.setSourceAddress(command ? new Address(commandSourceTon, commandSourceNpi, commandSourceAddress) - : new Address(sourceTon, sourceNpi, sourceAddress)); - submit.setDestAddress(new Address(destTon, destNpi, destAddress)); - SubmitSmResp submitResponce = getSession().submit(submit, submitTimeout); - if (submitResponce.getCommandStatus() == SmppConstants.STATUS_OK) { - LOGGER.info("SMS submitted, message id: " + submitResponce.getMessageId()); - } else { - throw new IllegalStateException(submitResponce.getResultMessage()); - } - } catch (SmppChannelException | RecoverablePduException - | SmppTimeoutException | UnrecoverablePduException error) { - throw new MessageException(error); - } - } else { - throw new MessageException(new SmppChannelException("SMPP session is not connected")); - } - } - - @Override - public void sendMessageAsync(final String destAddress, final String message, final boolean command) { - executorService.execute(new Runnable() { - @Override - public void run() { - try { - sendMessageSync(destAddress, message, command); - } catch (MessageException | InterruptedException | IllegalStateException error) { - LOGGER.warn("SMS sending error", error); - } - } - }); - } -} diff --git a/src/org/traccar/sms/HttpSmsClient.java b/src/org/traccar/sms/HttpSmsClient.java new file mode 100644 index 000000000..e14099772 --- /dev/null +++ b/src/org/traccar/sms/HttpSmsClient.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org) + * + * 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 org.traccar.sms; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.traccar.Context; +import org.traccar.api.SecurityRequestFilter; +import org.traccar.helper.DataConverter; +import org.traccar.notification.MessageException; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class HttpSmsClient implements SmsManager { + + private String url; + private String authorization; + private String template; + private boolean encode; + private MediaType mediaType; + + public HttpSmsClient() { + url = Context.getConfig().getString("sms.http.url"); + authorization = Context.getConfig().getString("sms.http.authorization"); + if (authorization == null) { + String user = Context.getConfig().getString("sms.http.user"); + String password = Context.getConfig().getString("sms.http.password"); + authorization = "Basic " + DataConverter.printBase64((user + ":" + password).getBytes()); + } + template = Context.getConfig().getString("sms.http.template").trim(); + if (template.charAt(0) == '{' || template.charAt(0) == '[') { + encode = false; + mediaType = MediaType.APPLICATION_JSON_TYPE; + } else { + encode = true; + mediaType = MediaType.APPLICATION_FORM_URLENCODED_TYPE; + } + } + + private String prepareValue(String value) throws UnsupportedEncodingException { + return encode ? URLEncoder.encode(value, StandardCharsets.UTF_8.name()) : value; + } + + private String preparePayload(String destAddress, String message) { + try { + return template + .replace("{phone}", prepareValue(destAddress)) + .replace("{message}", prepareValue(message)); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private Invocation.Builder getRequestBuilder() { + return Context.getClient().target(url).request() + .header(SecurityRequestFilter.AUTHORIZATION_HEADER, authorization); + } + + @Override + public void sendMessageSync(String destAddress, String message, boolean command) throws MessageException { + Response response = getRequestBuilder().post(Entity.entity(preparePayload(destAddress, message), mediaType)); + if (response.getStatus() / 100 != 2) { + throw new MessageException(response.readEntity(String.class)); + } + } + + @Override + public void sendMessageAsync(final String destAddress, final String message, final boolean command) { + getRequestBuilder().async().post(Entity.json(preparePayload(destAddress, message))); + } + +} diff --git a/src/org/traccar/sms/SmsGatewayClient.java b/src/org/traccar/sms/SmsGatewayClient.java deleted file mode 100644 index ea2df6881..000000000 --- a/src/org/traccar/sms/SmsGatewayClient.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2018 Anton Tananaev (anton@traccar.org) - * Copyright 2018 Andrey Kunitsyn (andrey@traccar.org) - * - * 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 org.traccar.sms; - -import javax.json.Json; -import javax.json.JsonArray; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.Invocation; -import javax.ws.rs.core.Response; -import org.traccar.Context; -import org.traccar.notification.MessageException; - -public class SmsGatewayClient implements SmsManager { - - private static final String URL = "https://smsgateway.me/api/v4/message/send"; - private static final String KEY_PHONE_NUMBER = "phone_number"; - private static final String KEY_MESSAGE = "message"; - private static final String KEY_DEVICE_ID = "device_id"; - private static final String KEY_AUTHORIZATION = "Authorization"; - - private final String token; - private final int deviceId; - - public SmsGatewayClient() { - token = Context.getConfig().getString("sms.smsgateway.token"); - deviceId = Context.getConfig().getInteger("sms.smsgateway.deviceId"); - } - - private JsonArray preparePayload(String destAddress, String message) { - return Json.createArrayBuilder() - .add(Json.createObjectBuilder() - .add(KEY_PHONE_NUMBER, destAddress) - .add(KEY_MESSAGE, message) - .add(KEY_DEVICE_ID, deviceId)) - .build(); - } - - private Invocation.Builder getRequestBuilder() { - return Context.getClient().target(URL).request() - .header(KEY_AUTHORIZATION, token); - } - - @Override - public void sendMessageSync(String destAddress, String message, boolean command) - throws InterruptedException, MessageException { - Response response = getRequestBuilder().post(Entity.json(preparePayload(destAddress, message))); - if (!response.getStatusInfo().equals(Response.Status.OK)) { - String output = response.readEntity(String.class); - throw new MessageException(output); - } - } - - @Override - public void sendMessageAsync(final String destAddress, final String message, final boolean command) { - getRequestBuilder().async().post(Entity.json(preparePayload(destAddress, message))); - } -} diff --git a/src/org/traccar/sms/SmsManager.java b/src/org/traccar/sms/SmsManager.java index 4bc4bd009..1e3d40f6e 100644 --- a/src/org/traccar/sms/SmsManager.java +++ b/src/org/traccar/sms/SmsManager.java @@ -20,8 +20,10 @@ import org.traccar.notification.MessageException; public interface SmsManager { - void sendMessageSync(String destAddress, String message, boolean command) - throws InterruptedException, MessageException; - void sendMessageAsync(final String destAddress, final String message, final boolean command); + void sendMessageSync( + String destAddress, String message, boolean command) throws InterruptedException, MessageException; + + void sendMessageAsync( + final String destAddress, final String message, final boolean command); } diff --git a/src/org/traccar/sms/smpp/ClientSmppSessionHandler.java b/src/org/traccar/sms/smpp/ClientSmppSessionHandler.java new file mode 100644 index 000000000..f48721ff9 --- /dev/null +++ b/src/org/traccar/sms/smpp/ClientSmppSessionHandler.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) + * + * 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 org.traccar.sms.smpp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.events.TextMessageEventHandler; + +import com.cloudhopper.commons.charset.CharsetUtil; +import com.cloudhopper.smpp.SmppConstants; +import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; +import com.cloudhopper.smpp.pdu.DeliverSm; +import com.cloudhopper.smpp.pdu.PduRequest; +import com.cloudhopper.smpp.pdu.PduResponse; +import com.cloudhopper.smpp.util.SmppUtil; + +public class ClientSmppSessionHandler extends DefaultSmppSessionHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ClientSmppSessionHandler.class); + + private SmppClient smppClient; + + public ClientSmppSessionHandler(SmppClient smppClient) { + this.smppClient = smppClient; + } + + @Override + public void firePduRequestExpired(PduRequest pduRequest) { + LOGGER.warn("PDU request expired: " + pduRequest); + } + + @Override + public PduResponse firePduRequestReceived(PduRequest request) { + PduResponse response; + try { + if (request instanceof DeliverSm) { + String sourceAddress = ((DeliverSm) request).getSourceAddress().getAddress(); + String message = CharsetUtil.decode(((DeliverSm) request).getShortMessage(), + smppClient.mapDataCodingToCharset(((DeliverSm) request).getDataCoding())); + LOGGER.info("SMS Message Received: " + message.trim() + ", Source Address: " + sourceAddress); + + boolean isDeliveryReceipt; + if (smppClient.getDetectDlrByOpts()) { + isDeliveryReceipt = request.getOptionalParameters() != null; + } else { + isDeliveryReceipt = SmppUtil.isMessageTypeAnyDeliveryReceipt(((DeliverSm) request).getEsmClass()); + } + + if (!isDeliveryReceipt) { + TextMessageEventHandler.handleTextMessage(sourceAddress, message); + } + } + response = request.createResponse(); + } catch (Exception error) { + LOGGER.warn("SMS receiving error", error); + response = request.createResponse(); + response.setResultMessage(error.getMessage()); + response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR); + } + return response; + } + + @Override + public void fireChannelUnexpectedlyClosed() { + LOGGER.warn("SMPP session channel unexpectedly closed"); + smppClient.scheduleReconnect(); + } + +} diff --git a/src/org/traccar/sms/smpp/EnquireLinkTask.java b/src/org/traccar/sms/smpp/EnquireLinkTask.java new file mode 100644 index 000000000..7086709d7 --- /dev/null +++ b/src/org/traccar/sms/smpp/EnquireLinkTask.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) + * + * 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 org.traccar.sms.smpp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cloudhopper.smpp.SmppSession; +import com.cloudhopper.smpp.pdu.EnquireLink; +import com.cloudhopper.smpp.type.RecoverablePduException; +import com.cloudhopper.smpp.type.SmppChannelException; +import com.cloudhopper.smpp.type.SmppTimeoutException; +import com.cloudhopper.smpp.type.UnrecoverablePduException; + +public class EnquireLinkTask implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(EnquireLinkTask.class); + + private SmppClient smppClient; + private Integer enquireLinkTimeout; + + public EnquireLinkTask(SmppClient smppClient, Integer enquireLinkTimeout) { + this.smppClient = smppClient; + this.enquireLinkTimeout = enquireLinkTimeout; + } + + @Override + public void run() { + SmppSession smppSession = smppClient.getSession(); + if (smppSession != null && smppSession.isBound()) { + try { + smppSession.enquireLink(new EnquireLink(), enquireLinkTimeout); + } catch (SmppTimeoutException | SmppChannelException + | RecoverablePduException | UnrecoverablePduException error) { + LOGGER.warn("Enquire link failed, executing reconnect: ", error); + smppClient.scheduleReconnect(); + } catch (InterruptedException error) { + LOGGER.info("Enquire link interrupted, probably killed by reconnecting"); + } + } else { + LOGGER.warn("Enquire link running while session is not connected"); + } + } + +} diff --git a/src/org/traccar/sms/smpp/ReconnectionTask.java b/src/org/traccar/sms/smpp/ReconnectionTask.java new file mode 100644 index 000000000..c009de8e7 --- /dev/null +++ b/src/org/traccar/sms/smpp/ReconnectionTask.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Anton Tananaev (anton@traccar.org) + * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org) + * + * 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 org.traccar.sms.smpp; + +public class ReconnectionTask implements Runnable { + + private final SmppClient smppClient; + + protected ReconnectionTask(SmppClient smppClient) { + this.smppClient = smppClient; + } + + @Override + public void run() { + smppClient.reconnect(); + } + +} diff --git a/src/org/traccar/sms/smpp/SmppClient.java b/src/org/traccar/sms/smpp/SmppClient.java new file mode 100644 index 000000000..874253d36 --- /dev/null +++ b/src/org/traccar/sms/smpp/SmppClient.java @@ -0,0 +1,272 @@ +/* + * Copyright 2017 - 2018 Anton Tananaev (anton@traccar.org) + * Copyright 2017 - 2018 Andrey Kunitsyn (andrey@traccar.org) + * + * 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 org.traccar.sms.smpp; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.traccar.Context; +import org.traccar.notification.MessageException; +import org.traccar.sms.SmsManager; + +import com.cloudhopper.commons.charset.CharsetUtil; +import com.cloudhopper.smpp.SmppBindType; +import com.cloudhopper.smpp.SmppConstants; +import com.cloudhopper.smpp.SmppSession; +import com.cloudhopper.smpp.SmppSessionConfiguration; +import com.cloudhopper.smpp.impl.DefaultSmppClient; +import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler; +import com.cloudhopper.smpp.pdu.SubmitSm; +import com.cloudhopper.smpp.pdu.SubmitSmResp; +import com.cloudhopper.smpp.tlv.Tlv; +import com.cloudhopper.smpp.type.Address; +import com.cloudhopper.smpp.type.RecoverablePduException; +import com.cloudhopper.smpp.type.SmppChannelException; +import com.cloudhopper.smpp.type.SmppTimeoutException; +import com.cloudhopper.smpp.type.UnrecoverablePduException; + +public class SmppClient implements SmsManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(SmppClient.class); + + private SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration(); + private SmppSession smppSession; + private DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler(this); + private ExecutorService executorService = Executors.newCachedThreadPool(); + private DefaultSmppClient clientBootstrap = new DefaultSmppClient(); + + private ScheduledExecutorService enquireLinkExecutor; + private ScheduledFuture enquireLinkTask; + private Integer enquireLinkPeriod; + private Integer enquireLinkTimeout; + + private ScheduledExecutorService reconnectionExecutor; + private ScheduledFuture reconnectionTask; + private Integer reconnectionDelay; + + private String sourceAddress; + private String commandSourceAddress; + private int submitTimeout; + private boolean requestDlr; + private boolean detectDlrByOpts; + private String notificationsCharsetName; + private byte notificationsDataCoding; + private String commandsCharsetName; + private byte commandsDataCoding; + + private byte sourceTon; + private byte sourceNpi; + private byte commandSourceTon; + private byte commandSourceNpi; + + private byte destTon; + private byte destNpi; + + public SmppClient() { + sessionConfig.setName("Traccar.smppSession"); + sessionConfig.setInterfaceVersion( + (byte) Context.getConfig().getInteger("sms.smpp.version", SmppConstants.VERSION_3_4)); + sessionConfig.setType(SmppBindType.TRANSCEIVER); + sessionConfig.setHost(Context.getConfig().getString("sms.smpp.host", "localhost")); + sessionConfig.setPort(Context.getConfig().getInteger("sms.smpp.port", 2775)); + sessionConfig.setSystemId(Context.getConfig().getString("sms.smpp.username", "user")); + sessionConfig.setSystemType(Context.getConfig().getString("sms.smpp.systemType", null)); + sessionConfig.setPassword(Context.getConfig().getString("sms.smpp.password", "password")); + sessionConfig.getLoggingOptions().setLogBytes(false); + sessionConfig.getLoggingOptions().setLogPdu(Context.getConfig().getBoolean("sms.smpp.logPdu")); + + sourceAddress = Context.getConfig().getString("sms.smpp.sourceAddress", ""); + commandSourceAddress = Context.getConfig().getString("sms.smpp.commandSourceAddress", sourceAddress); + submitTimeout = Context.getConfig().getInteger("sms.smpp.submitTimeout", 10000); + + requestDlr = Context.getConfig().getBoolean("sms.smpp.requestDlr"); + detectDlrByOpts = Context.getConfig().getBoolean("sms.smpp.detectDlrByOpts"); + + notificationsCharsetName = Context.getConfig().getString("sms.smpp.notificationsCharset", + CharsetUtil.NAME_UCS_2); + notificationsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.notificationsDataCoding", + SmppConstants.DATA_CODING_UCS2); + commandsCharsetName = Context.getConfig().getString("sms.smpp.commandsCharset", + CharsetUtil.NAME_GSM); + commandsDataCoding = (byte) Context.getConfig().getInteger("sms.smpp.commandsDataCoding", + SmppConstants.DATA_CODING_DEFAULT); + + + sourceTon = (byte) Context.getConfig().getInteger("sms.smpp.sourceTon", SmppConstants.TON_ALPHANUMERIC); + commandSourceTon = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceTon", sourceTon); + sourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.sourceNpi", SmppConstants.NPI_UNKNOWN); + commandSourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.commandSourceNpi", sourceNpi); + + destTon = (byte) Context.getConfig().getInteger("sms.smpp.destTon", SmppConstants.TON_INTERNATIONAL); + destNpi = (byte) Context.getConfig().getInteger("sms.smpp.destNpi", SmppConstants.NPI_E164); + + enquireLinkPeriod = Context.getConfig().getInteger("sms.smpp.enquireLinkPeriod", 60000); + enquireLinkTimeout = Context.getConfig().getInteger("sms.smpp.enquireLinkTimeout", 10000); + enquireLinkExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + String name = sessionConfig.getName(); + thread.setName("EnquireLink-" + name); + return thread; + } + }); + + reconnectionDelay = Context.getConfig().getInteger("sms.smpp.reconnectionDelay", 10000); + reconnectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + String name = sessionConfig.getName(); + thread.setName("Reconnection-" + name); + return thread; + } + }); + + scheduleReconnect(); + } + + public synchronized SmppSession getSession() { + return smppSession; + } + + public String mapDataCodingToCharset(byte dataCoding) { + switch (dataCoding) { + case SmppConstants.DATA_CODING_LATIN1: + return CharsetUtil.NAME_ISO_8859_1; + case SmppConstants.DATA_CODING_UCS2: + return CharsetUtil.NAME_UCS_2; + default: + return CharsetUtil.NAME_GSM; + } + } + + public boolean getDetectDlrByOpts() { + return detectDlrByOpts; + } + + protected synchronized void reconnect() { + try { + disconnect(); + smppSession = clientBootstrap.bind(sessionConfig, sessionHandler); + stopReconnectionkTask(); + runEnquireLinkTask(); + LOGGER.info("SMPP session connected"); + } catch (SmppTimeoutException | SmppChannelException + | UnrecoverablePduException | InterruptedException error) { + LOGGER.warn("Unable to connect to SMPP server: ", error); + } + } + + public void scheduleReconnect() { + if (reconnectionTask == null || reconnectionTask.isDone()) { + reconnectionTask = reconnectionExecutor.scheduleWithFixedDelay( + new ReconnectionTask(this), + reconnectionDelay, reconnectionDelay, TimeUnit.MILLISECONDS); + } + } + + private void stopReconnectionkTask() { + if (reconnectionTask != null) { + reconnectionTask.cancel(false); + } + } + + private void disconnect() { + stopEnquireLinkTask(); + destroySession(); + } + + private void runEnquireLinkTask() { + enquireLinkTask = enquireLinkExecutor.scheduleWithFixedDelay( + new EnquireLinkTask(this, enquireLinkTimeout), + enquireLinkPeriod, enquireLinkPeriod, TimeUnit.MILLISECONDS); + } + + private void stopEnquireLinkTask() { + if (enquireLinkTask != null) { + enquireLinkTask.cancel(true); + } + } + + private void destroySession() { + if (smppSession != null) { + LOGGER.info("Cleaning up SMPP session... "); + smppSession.destroy(); + smppSession = null; + } + } + + @Override + public synchronized void sendMessageSync(String destAddress, String message, boolean command) + throws MessageException, InterruptedException, IllegalStateException { + if (getSession() != null && getSession().isBound()) { + try { + SubmitSm submit = new SubmitSm(); + byte[] textBytes; + textBytes = CharsetUtil.encode(message, command ? commandsCharsetName : notificationsCharsetName); + submit.setDataCoding(command ? commandsDataCoding : notificationsDataCoding); + if (requestDlr) { + submit.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED); + } + + if (textBytes != null && textBytes.length > 255) { + submit.addOptionalParameter(new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, textBytes, + "message_payload")); + } else { + submit.setShortMessage(textBytes); + } + + submit.setSourceAddress(command ? new Address(commandSourceTon, commandSourceNpi, commandSourceAddress) + : new Address(sourceTon, sourceNpi, sourceAddress)); + submit.setDestAddress(new Address(destTon, destNpi, destAddress)); + SubmitSmResp submitResponce = getSession().submit(submit, submitTimeout); + if (submitResponce.getCommandStatus() == SmppConstants.STATUS_OK) { + LOGGER.info("SMS submitted, message id: " + submitResponce.getMessageId()); + } else { + throw new IllegalStateException(submitResponce.getResultMessage()); + } + } catch (SmppChannelException | RecoverablePduException + | SmppTimeoutException | UnrecoverablePduException error) { + throw new MessageException(error); + } + } else { + throw new MessageException(new SmppChannelException("SMPP session is not connected")); + } + } + + @Override + public void sendMessageAsync(final String destAddress, final String message, final boolean command) { + executorService.execute(new Runnable() { + @Override + public void run() { + try { + sendMessageSync(destAddress, message, command); + } catch (MessageException | InterruptedException | IllegalStateException error) { + LOGGER.warn("SMS sending error", error); + } + } + }); + } + +} -- cgit v1.2.3