Showing
36 changed files
with
4702 additions
and
1 deletions
@@ -708,6 +708,59 @@ transport: | @@ -708,6 +708,59 @@ transport: | ||
708 | # Skip certificate validity check for client certificates. | 708 | # Skip certificate validity check for client certificates. |
709 | skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" | 709 | skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" |
710 | # Local CoAP transport parameters | 710 | # Local CoAP transport parameters |
711 | + tcp: | ||
712 | + # Enable/disable tcp transport protocol. | ||
713 | + enabled: "${TCP_ENABLED:true}" | ||
714 | + bind_address: "${TCP_BIND_ADDRESS:0.0.0.0}" | ||
715 | + bind_port: "${TCP_BIND_PORT:8088}" | ||
716 | + # Enable proxy protocol support. Disabled by default. If enabled, supports both v1 and v2. | ||
717 | + # Useful to get the real IP address of the client in the logs and for rate limits. | ||
718 | + proxy_enabled: "${TCP_PROXY_PROTOCOL_ENABLED:false}" | ||
719 | + timeout: "${TCP_TIMEOUT:10000}" | ||
720 | + msg_queue_size_per_device_limit: "${TCP_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | ||
721 | + netty: | ||
722 | + leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" | ||
723 | + boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | ||
724 | + worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}" | ||
725 | + max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}" | ||
726 | + so_keep_alive: "${NETTY_SO_KEEPALIVE:false}" | ||
727 | + # TCP SSL configuration | ||
728 | + ssl: | ||
729 | + # Enable/disable SSL support | ||
730 | + enabled: "${TCP_SSL_ENABLED:false}" | ||
731 | + # TCP SSL bind address | ||
732 | + bind_address: "${TCP_SSL_BIND_ADDRESS:0.0.0.0}" | ||
733 | + # TCP SSL bind port | ||
734 | + bind_port: "${TCP_SSL_BIND_PORT:8888}" | ||
735 | + # SSL protocol: See https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms | ||
736 | + protocol: "${TCP_SSL_PROTOCOL:TLSv1.2}" | ||
737 | + # Server SSL credentials | ||
738 | + credentials: | ||
739 | + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) | ||
740 | + type: "${TCP_SSL_CREDENTIALS_TYPE:PEM}" | ||
741 | + # PEM server credentials | ||
742 | + pem: | ||
743 | + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) | ||
744 | + cert_file: "${TCP_SSL_PEM_CERT:tcpserver.pem}" | ||
745 | + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; | ||
746 | + key_file: "${TCP_SSL_PEM_KEY:tcpserver_key.pem}" | ||
747 | + # Server certificate private key password (optional) | ||
748 | + key_password: "${TCP_SSL_PEM_KEY_PASSWORD:server_key_password}" | ||
749 | + # Keystore server credentials | ||
750 | + keystore: | ||
751 | + # Type of the key store | ||
752 | + type: "${TCP_SSL_KEY_STORE_TYPE:JKS}" | ||
753 | + # Path to the key store that holds the SSL certificate | ||
754 | + store_file: "${TCP_SSL_KEY_STORE:tcpserver.jks}" | ||
755 | + # Password used to access the key store | ||
756 | + store_password: "${TCP_SSL_KEY_STORE_PASSWORD:server_ks_password}" | ||
757 | + # Optional alias of the private key; If not set, the platform will load the first private key from the keystore; | ||
758 | + key_alias: "${TCP_SSL_KEY_ALIAS:}" | ||
759 | + # Optional password to access the private key. If not set, the platform will attempt to load the private keys that are not protected with the password; | ||
760 | + key_password: "${TCP_SSL_KEY_PASSWORD:server_key_password}" | ||
761 | + # Skip certificate validity check for client certificates. | ||
762 | + skip_validity_check_for_client_cert: "${TCP_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" | ||
763 | + # Local CoAP transport parameters | ||
711 | coap: | 764 | coap: |
712 | # Enable/disable coap transport protocol. | 765 | # Enable/disable coap transport protocol. |
713 | enabled: "${COAP_ENABLED:true}" | 766 | enabled: "${COAP_ENABLED:true}" |
@@ -43,6 +43,8 @@ public class DataConstants { | @@ -43,6 +43,8 @@ public class DataConstants { | ||
43 | public static final String COAP_TRANSPORT_NAME = "COAP"; | 43 | public static final String COAP_TRANSPORT_NAME = "COAP"; |
44 | public static final String LWM2M_TRANSPORT_NAME = "LWM2M"; | 44 | public static final String LWM2M_TRANSPORT_NAME = "LWM2M"; |
45 | public static final String MQTT_TRANSPORT_NAME = "MQTT"; | 45 | public static final String MQTT_TRANSPORT_NAME = "MQTT"; |
46 | + //Thingskit function | ||
47 | + public static final String TCP_TRANSPORT_NAME = "TCP"; | ||
46 | public static final String HTTP_TRANSPORT_NAME = "HTTP"; | 48 | public static final String HTTP_TRANSPORT_NAME = "HTTP"; |
47 | public static final String SNMP_TRANSPORT_NAME = "SNMP"; | 49 | public static final String SNMP_TRANSPORT_NAME = "SNMP"; |
48 | 50 |
@@ -18,6 +18,7 @@ package org.thingsboard.server.common.data; | @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data; | ||
18 | public enum DeviceTransportType { | 18 | public enum DeviceTransportType { |
19 | DEFAULT, | 19 | DEFAULT, |
20 | MQTT, | 20 | MQTT, |
21 | + TCP, | ||
21 | COAP, | 22 | COAP, |
22 | LWM2M, | 23 | LWM2M, |
23 | SNMP | 24 | SNMP |
@@ -37,6 +37,7 @@ | @@ -37,6 +37,7 @@ | ||
37 | <modules> | 37 | <modules> |
38 | <module>transport-api</module> | 38 | <module>transport-api</module> |
39 | <module>mqtt</module> | 39 | <module>mqtt</module> |
40 | + <module>tcp</module> | ||
40 | <module>http</module> | 41 | <module>http</module> |
41 | <module>coap</module> | 42 | <module>coap</module> |
42 | <module>lwm2m</module> | 43 | <module>lwm2m</module> |
common/transport/tcp/pom.xml
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2022 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
19 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
20 | + <modelVersion>4.0.0</modelVersion> | ||
21 | + <parent> | ||
22 | + <groupId>org.thingsboard.common</groupId> | ||
23 | + <version>3.3.4-SNAPSHOT</version> | ||
24 | + <artifactId>transport</artifactId> | ||
25 | + </parent> | ||
26 | + <groupId>org.thingsboard.common.transport</groupId> | ||
27 | + <artifactId>tcp</artifactId> | ||
28 | + <packaging>jar</packaging> | ||
29 | + | ||
30 | + <name>Thingsboard TPC Transport Common</name> | ||
31 | + <url>https://thingsboard.io</url> | ||
32 | + | ||
33 | + <properties> | ||
34 | + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
35 | + <main.dir>${basedir}/../../..</main.dir> | ||
36 | + </properties> | ||
37 | + | ||
38 | + <dependencies> | ||
39 | + <dependency> | ||
40 | + <groupId>org.thingsboard.common.transport</groupId> | ||
41 | + <artifactId>transport-api</artifactId> | ||
42 | + </dependency> | ||
43 | + <dependency> | ||
44 | + <groupId>io.netty</groupId> | ||
45 | + <artifactId>netty-all</artifactId> | ||
46 | + </dependency> | ||
47 | + <dependency> | ||
48 | + <groupId>io.netty</groupId> | ||
49 | + <artifactId>netty-tcnative-boringssl-static</artifactId> | ||
50 | + </dependency> | ||
51 | + <dependency> | ||
52 | + <groupId>org.springframework</groupId> | ||
53 | + <artifactId>spring-context-support</artifactId> | ||
54 | + </dependency> | ||
55 | + <dependency> | ||
56 | + <groupId>org.springframework</groupId> | ||
57 | + <artifactId>spring-context</artifactId> | ||
58 | + </dependency> | ||
59 | + <dependency> | ||
60 | + <groupId>org.slf4j</groupId> | ||
61 | + <artifactId>slf4j-api</artifactId> | ||
62 | + </dependency> | ||
63 | + <dependency> | ||
64 | + <groupId>org.slf4j</groupId> | ||
65 | + <artifactId>log4j-over-slf4j</artifactId> | ||
66 | + </dependency> | ||
67 | + <dependency> | ||
68 | + <groupId>ch.qos.logback</groupId> | ||
69 | + <artifactId>logback-core</artifactId> | ||
70 | + </dependency> | ||
71 | + <dependency> | ||
72 | + <groupId>ch.qos.logback</groupId> | ||
73 | + <artifactId>logback-classic</artifactId> | ||
74 | + </dependency> | ||
75 | + <dependency> | ||
76 | + <groupId>com.google.guava</groupId> | ||
77 | + <artifactId>guava</artifactId> | ||
78 | + </dependency> | ||
79 | + <dependency> | ||
80 | + <groupId>com.google.code.findbugs</groupId> | ||
81 | + <artifactId>jsr305</artifactId> | ||
82 | + <version>3.0.1</version> | ||
83 | + <optional>true</optional> | ||
84 | + </dependency> | ||
85 | + <dependency> | ||
86 | + <groupId>org.springframework.boot</groupId> | ||
87 | + <artifactId>spring-boot-starter-test</artifactId> | ||
88 | + <scope>test</scope> | ||
89 | + </dependency> | ||
90 | + <dependency> | ||
91 | + <groupId>org.junit.vintage</groupId> | ||
92 | + <artifactId>junit-vintage-engine</artifactId> | ||
93 | + <scope>test</scope> | ||
94 | + </dependency> | ||
95 | + <dependency> | ||
96 | + <groupId>org.awaitility</groupId> | ||
97 | + <artifactId>awaitility</artifactId> | ||
98 | + <scope>test</scope> | ||
99 | + </dependency> | ||
100 | + </dependencies> | ||
101 | + | ||
102 | +</project> |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/TcpSslHandlerProvider.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import io.netty.handler.ssl.SslHandler; | ||
19 | +import lombok.extern.slf4j.Slf4j; | ||
20 | +import org.springframework.beans.factory.annotation.Autowired; | ||
21 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
22 | +import org.springframework.beans.factory.annotation.Value; | ||
23 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
24 | +import org.springframework.boot.context.properties.ConfigurationProperties; | ||
25 | +import org.springframework.context.annotation.Bean; | ||
26 | +import org.springframework.stereotype.Component; | ||
27 | +import org.springframework.util.StringUtils; | ||
28 | +import org.thingsboard.server.common.data.DeviceTransportType; | ||
29 | +import org.thingsboard.server.common.msg.EncryptionUtil; | ||
30 | +import org.thingsboard.server.common.transport.TransportService; | ||
31 | +import org.thingsboard.server.common.transport.TransportServiceCallback; | ||
32 | +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; | ||
33 | +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; | ||
34 | +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; | ||
35 | +import org.thingsboard.server.common.transport.util.SslUtil; | ||
36 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
37 | + | ||
38 | +import javax.net.ssl.KeyManager; | ||
39 | +import javax.net.ssl.KeyManagerFactory; | ||
40 | +import javax.net.ssl.SSLContext; | ||
41 | +import javax.net.ssl.SSLEngine; | ||
42 | +import javax.net.ssl.TrustManager; | ||
43 | +import javax.net.ssl.TrustManagerFactory; | ||
44 | +import javax.net.ssl.X509TrustManager; | ||
45 | +import java.security.cert.CertificateEncodingException; | ||
46 | +import java.security.cert.CertificateException; | ||
47 | +import java.security.cert.X509Certificate; | ||
48 | +import java.util.concurrent.CountDownLatch; | ||
49 | +import java.util.concurrent.TimeUnit; | ||
50 | + | ||
51 | +/** | ||
52 | + * Created by valerii.sosliuk on 11/6/16. | ||
53 | + */ | ||
54 | +@Slf4j | ||
55 | +@Component("TcpSslHandlerProvider") | ||
56 | +@ConditionalOnProperty(prefix = "transport.tcp.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) | ||
57 | +public class TcpSslHandlerProvider { | ||
58 | + | ||
59 | + @Value("${transport.tcp.ssl.protocol}") | ||
60 | + private String sslProtocol; | ||
61 | + | ||
62 | + @Autowired | ||
63 | + private TransportService transportService; | ||
64 | + | ||
65 | + @Bean | ||
66 | + @ConfigurationProperties(prefix = "transport.tcp.ssl.credentials") | ||
67 | + public SslCredentialsConfig tcpSslCredentials() { | ||
68 | + return new SslCredentialsConfig("tcp SSL Credentials", false); | ||
69 | + } | ||
70 | + | ||
71 | + @Autowired | ||
72 | + @Qualifier("tcpSslCredentials") | ||
73 | + private SslCredentialsConfig tcpSslCredentialsConfig; | ||
74 | + | ||
75 | + private SSLContext sslContext; | ||
76 | + | ||
77 | + public SslHandler getSslHandler() { | ||
78 | + if (sslContext == null) { | ||
79 | + sslContext = createSslContext(); | ||
80 | + } | ||
81 | + SSLEngine sslEngine = sslContext.createSSLEngine(); | ||
82 | + sslEngine.setUseClientMode(false); | ||
83 | + sslEngine.setNeedClientAuth(false); | ||
84 | + sslEngine.setWantClientAuth(true); | ||
85 | + sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols()); | ||
86 | + sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); | ||
87 | + sslEngine.setEnableSessionCreation(true); | ||
88 | + return new SslHandler(sslEngine); | ||
89 | + } | ||
90 | + | ||
91 | + private SSLContext createSslContext() { | ||
92 | + try { | ||
93 | + SslCredentials sslCredentials = this.tcpSslCredentialsConfig.getCredentials(); | ||
94 | + TrustManagerFactory tmFactory = sslCredentials.createTrustManagerFactory(); | ||
95 | + KeyManagerFactory kmf = sslCredentials.createKeyManagerFactory(); | ||
96 | + | ||
97 | + KeyManager[] km = kmf.getKeyManagers(); | ||
98 | + TrustManager x509wrapped = getX509TrustManager(tmFactory); | ||
99 | + TrustManager[] tm = {x509wrapped}; | ||
100 | + if (StringUtils.isEmpty(sslProtocol)) { | ||
101 | + sslProtocol = "TLS"; | ||
102 | + } | ||
103 | + SSLContext sslContext = SSLContext.getInstance(sslProtocol); | ||
104 | + sslContext.init(km, tm, null); | ||
105 | + return sslContext; | ||
106 | + } catch (Exception e) { | ||
107 | + log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e); | ||
108 | + throw new RuntimeException("Failed to get SSL context", e); | ||
109 | + } | ||
110 | + } | ||
111 | + | ||
112 | + private TrustManager getX509TrustManager(TrustManagerFactory tmf) throws Exception { | ||
113 | + X509TrustManager x509Tm = null; | ||
114 | + for (TrustManager tm : tmf.getTrustManagers()) { | ||
115 | + if (tm instanceof X509TrustManager) { | ||
116 | + x509Tm = (X509TrustManager) tm; | ||
117 | + break; | ||
118 | + } | ||
119 | + } | ||
120 | + return new ThingsboardTcpX509TrustManager(x509Tm, transportService); | ||
121 | + } | ||
122 | + | ||
123 | + static class ThingsboardTcpX509TrustManager implements X509TrustManager { | ||
124 | + | ||
125 | + private final X509TrustManager trustManager; | ||
126 | + private TransportService transportService; | ||
127 | + | ||
128 | + ThingsboardTcpX509TrustManager(X509TrustManager trustManager, TransportService transportService) { | ||
129 | + this.trustManager = trustManager; | ||
130 | + this.transportService = transportService; | ||
131 | + } | ||
132 | + | ||
133 | + @Override | ||
134 | + public X509Certificate[] getAcceptedIssuers() { | ||
135 | + return trustManager.getAcceptedIssuers(); | ||
136 | + } | ||
137 | + | ||
138 | + @Override | ||
139 | + public void checkServerTrusted(X509Certificate[] chain, | ||
140 | + String authType) throws CertificateException { | ||
141 | + trustManager.checkServerTrusted(chain, authType); | ||
142 | + } | ||
143 | + | ||
144 | + @Override | ||
145 | + public void checkClientTrusted(X509Certificate[] chain, | ||
146 | + String authType) throws CertificateException { | ||
147 | + String credentialsBody = null; | ||
148 | + for (X509Certificate cert : chain) { | ||
149 | + try { | ||
150 | + String strCert = SslUtil.getCertificateString(cert); | ||
151 | + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); | ||
152 | + final String[] credentialsBodyHolder = new String[1]; | ||
153 | + CountDownLatch latch = new CountDownLatch(1); | ||
154 | + transportService.process(DeviceTransportType.TCP, TransportProtos.ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), | ||
155 | + new TransportServiceCallback<ValidateDeviceCredentialsResponse>() { | ||
156 | + @Override | ||
157 | + public void onSuccess(ValidateDeviceCredentialsResponse msg) { | ||
158 | + if (!StringUtils.isEmpty(msg.getCredentials())) { | ||
159 | + credentialsBodyHolder[0] = msg.getCredentials(); | ||
160 | + } | ||
161 | + latch.countDown(); | ||
162 | + } | ||
163 | + | ||
164 | + @Override | ||
165 | + public void onError(Throwable e) { | ||
166 | + log.error(e.getMessage(), e); | ||
167 | + latch.countDown(); | ||
168 | + } | ||
169 | + }); | ||
170 | + latch.await(10, TimeUnit.SECONDS); | ||
171 | + if (strCert.equals(credentialsBodyHolder[0])) { | ||
172 | + credentialsBody = credentialsBodyHolder[0]; | ||
173 | + break; | ||
174 | + } | ||
175 | + } catch (InterruptedException | CertificateEncodingException e) { | ||
176 | + log.error(e.getMessage(), e); | ||
177 | + } | ||
178 | + } | ||
179 | + if (credentialsBody == null) { | ||
180 | + throw new CertificateException("Invalid Device Certificate"); | ||
181 | + } | ||
182 | + } | ||
183 | + } | ||
184 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/TcpTransportContext.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import io.netty.handler.ssl.SslHandler; | ||
19 | +import lombok.Getter; | ||
20 | +import lombok.Setter; | ||
21 | +import lombok.extern.slf4j.Slf4j; | ||
22 | +import org.springframework.beans.factory.annotation.Autowired; | ||
23 | +import org.springframework.beans.factory.annotation.Value; | ||
24 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | ||
25 | +import org.springframework.stereotype.Component; | ||
26 | +import org.thingsboard.server.common.transport.TransportContext; | ||
27 | +import org.thingsboard.server.transport.tcp.adaptors.JsonTcpAdaptor; | ||
28 | +import org.thingsboard.server.transport.tcp.adaptors.ProtoTcpAdaptor; | ||
29 | + | ||
30 | +import javax.annotation.PostConstruct; | ||
31 | +import java.net.InetSocketAddress; | ||
32 | +import java.util.concurrent.atomic.AtomicInteger; | ||
33 | + | ||
34 | +/** | ||
35 | + * Created by ashvayka on 04.10.18. | ||
36 | + */ | ||
37 | +@Slf4j | ||
38 | +@Component | ||
39 | +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.tcp.enabled}'=='true')") | ||
40 | +public class TcpTransportContext extends TransportContext { | ||
41 | + | ||
42 | + @Getter | ||
43 | + @Autowired(required = false) | ||
44 | + private TcpSslHandlerProvider sslHandlerProvider; | ||
45 | + | ||
46 | + @Getter | ||
47 | + @Autowired | ||
48 | + private JsonTcpAdaptor jsonMqttAdaptor; | ||
49 | + | ||
50 | + @Getter | ||
51 | + @Autowired | ||
52 | + private ProtoTcpAdaptor protoMqttAdaptor; | ||
53 | + | ||
54 | + @Getter | ||
55 | + @Value("${transport.mqtt.netty.max_payload_size}") | ||
56 | + private Integer maxPayloadSize; | ||
57 | + | ||
58 | + @Getter | ||
59 | + @Value("${transport.mqtt.ssl.skip_validity_check_for_client_cert:false}") | ||
60 | + private boolean skipValidityCheckForClientCert; | ||
61 | + | ||
62 | + @Getter | ||
63 | + @Setter | ||
64 | + private SslHandler sslHandler; | ||
65 | + | ||
66 | + @Getter | ||
67 | + @Value("${transport.mqtt.msg_queue_size_per_device_limit:100}") | ||
68 | + private int messageQueueSizePerDeviceLimit; | ||
69 | + | ||
70 | + @Getter | ||
71 | + @Value("${transport.mqtt.timeout:10000}") | ||
72 | + private long timeout; | ||
73 | + | ||
74 | + @Getter | ||
75 | + @Value("${transport.mqtt.proxy_enabled:false}") | ||
76 | + private boolean proxyEnabled; | ||
77 | + | ||
78 | + private final AtomicInteger connectionsCounter = new AtomicInteger(); | ||
79 | + | ||
80 | + @PostConstruct | ||
81 | + public void init() { | ||
82 | + super.init(); | ||
83 | + transportService.createGaugeStats("openConnections", connectionsCounter); | ||
84 | + } | ||
85 | + | ||
86 | + public void channelRegistered() { | ||
87 | + connectionsCounter.incrementAndGet(); | ||
88 | + } | ||
89 | + | ||
90 | + public void channelUnregistered() { | ||
91 | + connectionsCounter.decrementAndGet(); | ||
92 | + } | ||
93 | + | ||
94 | + public boolean checkAddress(InetSocketAddress address) { | ||
95 | + return rateLimitService.checkAddress(address); | ||
96 | + } | ||
97 | + | ||
98 | + public void onAuthSuccess(InetSocketAddress address) { | ||
99 | + rateLimitService.onAuthSuccess(address); | ||
100 | + } | ||
101 | + | ||
102 | + public void onAuthFailure(InetSocketAddress address) { | ||
103 | + rateLimitService.onAuthFailure(address); | ||
104 | + } | ||
105 | + | ||
106 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/TcpTransportHandler.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import com.fasterxml.jackson.databind.JsonNode; | ||
19 | +import com.google.gson.JsonParseException; | ||
20 | +import io.netty.channel.ChannelFuture; | ||
21 | +import io.netty.channel.ChannelHandlerContext; | ||
22 | +import io.netty.channel.ChannelInboundHandlerAdapter; | ||
23 | +import io.netty.handler.codec.mqtt.MqttConnAckMessage; | ||
24 | +import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; | ||
25 | +import io.netty.handler.codec.mqtt.MqttConnectMessage; | ||
26 | +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; | ||
27 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | ||
28 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
29 | +import io.netty.handler.codec.mqtt.MqttMessageIdVariableHeader; | ||
30 | +import io.netty.handler.codec.mqtt.MqttPubAckMessage; | ||
31 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
32 | +import io.netty.handler.codec.mqtt.MqttQoS; | ||
33 | +import io.netty.handler.codec.mqtt.MqttSubAckMessage; | ||
34 | +import io.netty.handler.codec.mqtt.MqttSubAckPayload; | ||
35 | +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; | ||
36 | +import io.netty.handler.codec.mqtt.MqttTopicSubscription; | ||
37 | +import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; | ||
38 | +import io.netty.handler.ssl.SslHandler; | ||
39 | +import io.netty.util.CharsetUtil; | ||
40 | +import io.netty.util.ReferenceCountUtil; | ||
41 | +import io.netty.util.concurrent.Future; | ||
42 | +import io.netty.util.concurrent.GenericFutureListener; | ||
43 | +import lombok.extern.slf4j.Slf4j; | ||
44 | +import org.apache.commons.lang3.StringUtils; | ||
45 | +import org.thingsboard.server.common.data.DataConstants; | ||
46 | +import org.thingsboard.server.common.data.Device; | ||
47 | +import org.thingsboard.server.common.data.DeviceProfile; | ||
48 | +import org.thingsboard.server.common.data.DeviceTransportType; | ||
49 | +import org.thingsboard.server.common.data.TransportPayloadType; | ||
50 | +import org.thingsboard.server.common.data.device.profile.MqttTopics; | ||
51 | +import org.thingsboard.server.common.data.id.DeviceId; | ||
52 | +import org.thingsboard.server.common.data.id.OtaPackageId; | ||
53 | +import org.thingsboard.server.common.data.ota.OtaPackageType; | ||
54 | +import org.thingsboard.server.common.data.rpc.RpcStatus; | ||
55 | +import org.thingsboard.server.common.msg.EncryptionUtil; | ||
56 | +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; | ||
57 | +import org.thingsboard.server.common.transport.SessionMsgListener; | ||
58 | +import org.thingsboard.server.common.transport.TransportService; | ||
59 | +import org.thingsboard.server.common.transport.TransportServiceCallback; | ||
60 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
61 | +import org.thingsboard.server.common.transport.auth.SessionInfoCreator; | ||
62 | +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; | ||
63 | +import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; | ||
64 | +import org.thingsboard.server.common.transport.service.DefaultTransportService; | ||
65 | +import org.thingsboard.server.common.transport.service.SessionMetaData; | ||
66 | +import org.thingsboard.server.common.transport.util.SslUtil; | ||
67 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
68 | +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; | ||
69 | +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; | ||
70 | +import org.thingsboard.server.queue.scheduler.SchedulerComponent; | ||
71 | +import org.thingsboard.server.transport.tcp.adaptors.TcpTransportAdaptor; | ||
72 | +import org.thingsboard.server.transport.tcp.session.DeviceSessionCtx; | ||
73 | +import org.thingsboard.server.transport.tcp.session.GatewaySessionHandler; | ||
74 | +import org.thingsboard.server.transport.tcp.session.MqttTopicMatcher; | ||
75 | + | ||
76 | +import javax.net.ssl.SSLPeerUnverifiedException; | ||
77 | +import java.io.IOException; | ||
78 | +import java.net.InetSocketAddress; | ||
79 | +import java.security.cert.Certificate; | ||
80 | +import java.security.cert.X509Certificate; | ||
81 | +import java.util.ArrayList; | ||
82 | +import java.util.List; | ||
83 | +import java.util.Optional; | ||
84 | +import java.util.UUID; | ||
85 | +import java.util.concurrent.Callable; | ||
86 | +import java.util.concurrent.ConcurrentHashMap; | ||
87 | +import java.util.concurrent.ConcurrentMap; | ||
88 | +import java.util.concurrent.TimeUnit; | ||
89 | +import java.util.regex.Matcher; | ||
90 | +import java.util.regex.Pattern; | ||
91 | + | ||
92 | +import static com.amazonaws.util.StringUtils.UTF8; | ||
93 | +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_ACCEPTED; | ||
94 | +import static io.netty.handler.codec.mqtt.MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED; | ||
95 | +import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; | ||
96 | +import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; | ||
97 | +import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; | ||
98 | +import static io.netty.handler.codec.mqtt.MqttMessageType.PUBACK; | ||
99 | +import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; | ||
100 | +import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; | ||
101 | +import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; | ||
102 | +import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; | ||
103 | +import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE; | ||
104 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; | ||
105 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; | ||
106 | + | ||
107 | +/** | ||
108 | + * @author Andrew Shvayka | ||
109 | + */ | ||
110 | +@Slf4j | ||
111 | +public class TcpTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>>, SessionMsgListener { | ||
112 | + | ||
113 | + private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN); | ||
114 | + private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN); | ||
115 | + | ||
116 | + | ||
117 | + private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"; | ||
118 | + | ||
119 | + private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; | ||
120 | + | ||
121 | + private final UUID sessionId; | ||
122 | + private final TcpTransportContext context; | ||
123 | + private final TransportService transportService; | ||
124 | + private final SchedulerComponent scheduler; | ||
125 | + private final SslHandler sslHandler; | ||
126 | + private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; | ||
127 | + | ||
128 | + final DeviceSessionCtx deviceSessionCtx; | ||
129 | + volatile InetSocketAddress address; | ||
130 | + volatile GatewaySessionHandler gatewaySessionHandler; | ||
131 | + | ||
132 | + private final ConcurrentHashMap<String, String> otaPackSessions; | ||
133 | + private final ConcurrentHashMap<String, Integer> chunkSizes; | ||
134 | + private final ConcurrentMap<Integer, TransportProtos.ToDeviceRpcRequestMsg> rpcAwaitingAck; | ||
135 | + | ||
136 | + private TopicType attrSubTopicType; | ||
137 | + private TopicType rpcSubTopicType; | ||
138 | + private TopicType attrReqTopicType; | ||
139 | + private TopicType toServerRpcSubTopicType; | ||
140 | + | ||
141 | + TcpTransportHandler(TcpTransportContext context, SslHandler sslHandler) { | ||
142 | + this.sessionId = UUID.randomUUID(); | ||
143 | + this.context = context; | ||
144 | + this.transportService = context.getTransportService(); | ||
145 | + this.scheduler = context.getScheduler(); | ||
146 | + this.sslHandler = sslHandler; | ||
147 | + this.mqttQoSMap = new ConcurrentHashMap<>(); | ||
148 | + this.deviceSessionCtx = new DeviceSessionCtx(sessionId, mqttQoSMap, context); | ||
149 | + this.otaPackSessions = new ConcurrentHashMap<>(); | ||
150 | + this.chunkSizes = new ConcurrentHashMap<>(); | ||
151 | + this.rpcAwaitingAck = new ConcurrentHashMap<>(); | ||
152 | + } | ||
153 | + | ||
154 | + @Override | ||
155 | + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { | ||
156 | + super.channelRegistered(ctx); | ||
157 | + context.channelRegistered(); | ||
158 | + } | ||
159 | + | ||
160 | + @Override | ||
161 | + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { | ||
162 | + super.channelUnregistered(ctx); | ||
163 | + context.channelUnregistered(); | ||
164 | + } | ||
165 | + | ||
166 | + @Override | ||
167 | + public void channelRead(ChannelHandlerContext ctx, Object msg) { | ||
168 | + log.trace("[{}] Processing msg: {}", sessionId, msg); | ||
169 | + if (address == null) { | ||
170 | + address = getAddress(ctx); | ||
171 | + } | ||
172 | + try { | ||
173 | + if (msg instanceof MqttMessage) { | ||
174 | + MqttMessage message = (MqttMessage) msg; | ||
175 | + if (message.decoderResult().isSuccess()) { | ||
176 | + processMqttMsg(ctx, message); | ||
177 | + } else { | ||
178 | + log.error("[{}] Message decoding failed: {}", sessionId, message.decoderResult().cause().getMessage()); | ||
179 | + ctx.close(); | ||
180 | + } | ||
181 | + } else { | ||
182 | + log.debug("[{}] Received non mqtt message: {}", sessionId, msg.getClass().getSimpleName()); | ||
183 | + ctx.close(); | ||
184 | + } | ||
185 | + } finally { | ||
186 | + ReferenceCountUtil.safeRelease(msg); | ||
187 | + } | ||
188 | + } | ||
189 | + | ||
190 | + InetSocketAddress getAddress(ChannelHandlerContext ctx) { | ||
191 | + var address = ctx.channel().attr(TcpTransportService.ADDRESS).get(); | ||
192 | + if (address == null) { | ||
193 | + log.trace("[{}] Received empty address.", ctx.channel().id()); | ||
194 | + InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress(); | ||
195 | + log.trace("[{}] Going to use address: {}", ctx.channel().id(), remoteAddress); | ||
196 | + return remoteAddress; | ||
197 | + } else { | ||
198 | + log.trace("[{}] Received address: {}", ctx.channel().id(), address); | ||
199 | + } | ||
200 | + return address; | ||
201 | + } | ||
202 | + | ||
203 | + void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
204 | + if (msg.fixedHeader() == null) { | ||
205 | + log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); | ||
206 | + ctx.close(); | ||
207 | + return; | ||
208 | + } | ||
209 | + deviceSessionCtx.setChannel(ctx); | ||
210 | + if (CONNECT.equals(msg.fixedHeader().messageType())) { | ||
211 | + processConnect(ctx, (MqttConnectMessage) msg); | ||
212 | + } else if (deviceSessionCtx.isProvisionOnly()) { | ||
213 | + processProvisionSessionMsg(ctx, msg); | ||
214 | + } else { | ||
215 | + enqueueRegularSessionMsg(ctx, msg); | ||
216 | + } | ||
217 | + } | ||
218 | + | ||
219 | + private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
220 | + switch (msg.fixedHeader().messageType()) { | ||
221 | + case PUBLISH: | ||
222 | + MqttPublishMessage mqttMsg = (MqttPublishMessage) msg; | ||
223 | + String topicName = mqttMsg.variableHeader().topicName(); | ||
224 | + int msgId = mqttMsg.variableHeader().packetId(); | ||
225 | + try { | ||
226 | + if (topicName.equals(MqttTopics.DEVICE_PROVISION_REQUEST_TOPIC)) { | ||
227 | + try { | ||
228 | + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); | ||
229 | + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); | ||
230 | + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); | ||
231 | + } catch (Exception e) { | ||
232 | + if (e instanceof JsonParseException || (e.getCause() != null && e.getCause() instanceof JsonParseException)) { | ||
233 | + TransportProtos.ProvisionDeviceRequestMsg provisionRequestMsg = deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToProvisionRequestMsg(deviceSessionCtx, mqttMsg); | ||
234 | + transportService.process(provisionRequestMsg, new DeviceProvisionCallback(ctx, msgId, provisionRequestMsg)); | ||
235 | + deviceSessionCtx.setProvisionPayloadType(TransportPayloadType.PROTOBUF); | ||
236 | + log.trace("[{}][{}] Processing provision publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); | ||
237 | + } else { | ||
238 | + throw e; | ||
239 | + } | ||
240 | + } | ||
241 | + } else { | ||
242 | + log.debug("[{}] Unsupported topic for provisioning requests: {}!", sessionId, topicName); | ||
243 | + ctx.close(); | ||
244 | + } | ||
245 | + } catch (RuntimeException e) { | ||
246 | + log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ||
247 | + ctx.close(); | ||
248 | + } catch (AdaptorException e) { | ||
249 | + log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ||
250 | + ctx.close(); | ||
251 | + } | ||
252 | + break; | ||
253 | + case PINGREQ: | ||
254 | + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); | ||
255 | + break; | ||
256 | + case DISCONNECT: | ||
257 | + ctx.close(); | ||
258 | + break; | ||
259 | + } | ||
260 | + } | ||
261 | + | ||
262 | + void enqueueRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
263 | + final int queueSize = deviceSessionCtx.getMsgQueueSize(); | ||
264 | + if (queueSize >= context.getMessageQueueSizePerDeviceLimit()) { | ||
265 | + log.info("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}", | ||
266 | + deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueueSize()); | ||
267 | + ctx.close(); | ||
268 | + return; | ||
269 | + } | ||
270 | + | ||
271 | + deviceSessionCtx.addToQueue(msg); | ||
272 | + processMsgQueue(ctx); //Under the normal conditions the msg queue will contain 0 messages. Many messages will be processed on device connect event in separate thread pool | ||
273 | + } | ||
274 | + | ||
275 | + void processMsgQueue(ChannelHandlerContext ctx) { | ||
276 | + if (!deviceSessionCtx.isConnected()) { | ||
277 | + log.trace("[{}][{}] Postpone processing msg due to device is not connected. Msg queue size is {}", sessionId, deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueueSize()); | ||
278 | + return; | ||
279 | + } | ||
280 | + deviceSessionCtx.tryProcessQueuedMsgs(msg -> processRegularSessionMsg(ctx, msg)); | ||
281 | + } | ||
282 | + | ||
283 | + void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
284 | + switch (msg.fixedHeader().messageType()) { | ||
285 | + case PUBLISH: | ||
286 | + processPublish(ctx, (MqttPublishMessage) msg); | ||
287 | + break; | ||
288 | + case SUBSCRIBE: | ||
289 | + processSubscribe(ctx, (MqttSubscribeMessage) msg); | ||
290 | + break; | ||
291 | + case UNSUBSCRIBE: | ||
292 | + processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg); | ||
293 | + break; | ||
294 | + case PINGREQ: | ||
295 | + if (checkConnected(ctx, msg)) { | ||
296 | + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); | ||
297 | + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); | ||
298 | + } | ||
299 | + break; | ||
300 | + case DISCONNECT: | ||
301 | + ctx.close(); | ||
302 | + break; | ||
303 | + case PUBACK: | ||
304 | + int msgId = ((MqttPubAckMessage) msg).variableHeader().messageId(); | ||
305 | + TransportProtos.ToDeviceRpcRequestMsg rpcRequest = rpcAwaitingAck.remove(msgId); | ||
306 | + if (rpcRequest != null) { | ||
307 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); | ||
308 | + } | ||
309 | + break; | ||
310 | + default: | ||
311 | + break; | ||
312 | + } | ||
313 | + } | ||
314 | + | ||
315 | + private void processPublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg) { | ||
316 | + if (!checkConnected(ctx, mqttMsg)) { | ||
317 | + return; | ||
318 | + } | ||
319 | + String topicName = mqttMsg.variableHeader().topicName(); | ||
320 | + int msgId = mqttMsg.variableHeader().packetId(); | ||
321 | + log.trace("[{}][{}] Processing publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); | ||
322 | + | ||
323 | + if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) { | ||
324 | + if (gatewaySessionHandler != null) { | ||
325 | + handleGatewayPublishMsg(ctx, topicName, msgId, mqttMsg); | ||
326 | + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); | ||
327 | + } | ||
328 | + } else { | ||
329 | + processDevicePublish(ctx, mqttMsg, topicName, msgId); | ||
330 | + } | ||
331 | + } | ||
332 | + | ||
333 | + private void handleGatewayPublishMsg(ChannelHandlerContext ctx, String topicName, int msgId, MqttPublishMessage mqttMsg) { | ||
334 | + try { | ||
335 | + switch (topicName) { | ||
336 | + case MqttTopics.GATEWAY_TELEMETRY_TOPIC: | ||
337 | + gatewaySessionHandler.onDeviceTelemetry(mqttMsg); | ||
338 | + break; | ||
339 | + case MqttTopics.GATEWAY_CLAIM_TOPIC: | ||
340 | + gatewaySessionHandler.onDeviceClaim(mqttMsg); | ||
341 | + break; | ||
342 | + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: | ||
343 | + gatewaySessionHandler.onDeviceAttributes(mqttMsg); | ||
344 | + break; | ||
345 | + case MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC: | ||
346 | + gatewaySessionHandler.onDeviceAttributesRequest(mqttMsg); | ||
347 | + break; | ||
348 | + case MqttTopics.GATEWAY_RPC_TOPIC: | ||
349 | + gatewaySessionHandler.onDeviceRpcResponse(mqttMsg); | ||
350 | + break; | ||
351 | + case MqttTopics.GATEWAY_CONNECT_TOPIC: | ||
352 | + gatewaySessionHandler.onDeviceConnect(mqttMsg); | ||
353 | + break; | ||
354 | + case MqttTopics.GATEWAY_DISCONNECT_TOPIC: | ||
355 | + gatewaySessionHandler.onDeviceDisconnect(mqttMsg); | ||
356 | + break; | ||
357 | + default: | ||
358 | + ack(ctx, msgId); | ||
359 | + } | ||
360 | + } catch (RuntimeException e) { | ||
361 | + log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ||
362 | + ctx.close(); | ||
363 | + } catch (AdaptorException e) { | ||
364 | + log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ||
365 | + ctx.close(); | ||
366 | + } | ||
367 | + } | ||
368 | + | ||
369 | + private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { | ||
370 | + try { | ||
371 | + Matcher fwMatcher; | ||
372 | + TcpTransportAdaptor payloadAdaptor = deviceSessionCtx.getPayloadAdaptor(); | ||
373 | + if (deviceSessionCtx.isDeviceAttributesTopic(topicName)) { | ||
374 | + TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); | ||
375 | + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); | ||
376 | + } else if (deviceSessionCtx.isDeviceTelemetryTopic(topicName)) { | ||
377 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); | ||
378 | + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); | ||
379 | + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { | ||
380 | + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); | ||
381 | + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); | ||
382 | + attrReqTopicType = TopicType.V1; | ||
383 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC)) { | ||
384 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); | ||
385 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); | ||
386 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) { | ||
387 | + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); | ||
388 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); | ||
389 | + toServerRpcSubTopicType = TopicType.V1; | ||
390 | + } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { | ||
391 | + TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); | ||
392 | + transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); | ||
393 | + } else if ((fwMatcher = FW_REQUEST_PATTERN.matcher(topicName)).find()) { | ||
394 | + getOtaPackageCallback(ctx, mqttMsg, msgId, fwMatcher, OtaPackageType.FIRMWARE); | ||
395 | + } else if ((fwMatcher = SW_REQUEST_PATTERN.matcher(topicName)).find()) { | ||
396 | + getOtaPackageCallback(ctx, mqttMsg, msgId, fwMatcher, OtaPackageType.SOFTWARE); | ||
397 | + } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC)) { | ||
398 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); | ||
399 | + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); | ||
400 | + } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC)) { | ||
401 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = context.getJsonMqttAdaptor().convertToPostTelemetry(deviceSessionCtx, mqttMsg); | ||
402 | + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); | ||
403 | + } else if (topicName.equals(MqttTopics.DEVICE_TELEMETRY_SHORT_PROTO_TOPIC)) { | ||
404 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = context.getProtoMqttAdaptor().convertToPostTelemetry(deviceSessionCtx, mqttMsg); | ||
405 | + transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); | ||
406 | + } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC)) { | ||
407 | + TransportProtos.PostAttributeMsg postAttributeMsg = payloadAdaptor.convertToPostAttributes(deviceSessionCtx, mqttMsg); | ||
408 | + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); | ||
409 | + } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC)) { | ||
410 | + TransportProtos.PostAttributeMsg postAttributeMsg = context.getJsonMqttAdaptor().convertToPostAttributes(deviceSessionCtx, mqttMsg); | ||
411 | + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); | ||
412 | + } else if (topicName.equals(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC)) { | ||
413 | + TransportProtos.PostAttributeMsg postAttributeMsg = context.getProtoMqttAdaptor().convertToPostAttributes(deviceSessionCtx, mqttMsg); | ||
414 | + transportService.process(deviceSessionCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(ctx, msgId, postAttributeMsg)); | ||
415 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC)) { | ||
416 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = context.getJsonMqttAdaptor().convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC); | ||
417 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); | ||
418 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC)) { | ||
419 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = context.getProtoMqttAdaptor().convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC); | ||
420 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); | ||
421 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC)) { | ||
422 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC); | ||
423 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); | ||
424 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC)) { | ||
425 | + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = context.getJsonMqttAdaptor().convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC); | ||
426 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); | ||
427 | + toServerRpcSubTopicType = TopicType.V2_JSON; | ||
428 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC)) { | ||
429 | + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = context.getProtoMqttAdaptor().convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC); | ||
430 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); | ||
431 | + toServerRpcSubTopicType = TopicType.V2_PROTO; | ||
432 | + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC)) { | ||
433 | + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC); | ||
434 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); | ||
435 | + toServerRpcSubTopicType = TopicType.V2; | ||
436 | + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX)) { | ||
437 | + TransportProtos.GetAttributeRequestMsg getAttributeMsg = context.getJsonMqttAdaptor().convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); | ||
438 | + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); | ||
439 | + attrReqTopicType = TopicType.V2_JSON; | ||
440 | + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX)) { | ||
441 | + TransportProtos.GetAttributeRequestMsg getAttributeMsg = context.getProtoMqttAdaptor().convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX); | ||
442 | + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); | ||
443 | + attrReqTopicType = TopicType.V2_PROTO; | ||
444 | + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX)) { | ||
445 | + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); | ||
446 | + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); | ||
447 | + attrReqTopicType = TopicType.V2; | ||
448 | + } else { | ||
449 | + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); | ||
450 | + ack(ctx, msgId); | ||
451 | + } | ||
452 | + } catch (AdaptorException e) { | ||
453 | + log.debug("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | ||
454 | + log.info("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId); | ||
455 | + ctx.close(); | ||
456 | + } | ||
457 | + } | ||
458 | + | ||
459 | + private void getOtaPackageCallback(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, int msgId, Matcher fwMatcher, OtaPackageType type) { | ||
460 | + String payload = mqttMsg.content().toString(UTF8); | ||
461 | + int chunkSize = StringUtils.isNotEmpty(payload) ? Integer.parseInt(payload) : 0; | ||
462 | + String requestId = fwMatcher.group("requestId"); | ||
463 | + int chunk = Integer.parseInt(fwMatcher.group("chunk")); | ||
464 | + | ||
465 | + if (chunkSize > 0) { | ||
466 | + this.chunkSizes.put(requestId, chunkSize); | ||
467 | + } else { | ||
468 | + chunkSize = chunkSizes.getOrDefault(requestId, 0); | ||
469 | + } | ||
470 | + | ||
471 | + if (chunkSize > context.getMaxPayloadSize()) { | ||
472 | + sendOtaPackageError(ctx, PAYLOAD_TOO_LARGE); | ||
473 | + return; | ||
474 | + } | ||
475 | + | ||
476 | + String otaPackageId = otaPackSessions.get(requestId); | ||
477 | + | ||
478 | + if (otaPackageId != null) { | ||
479 | + sendOtaPackage(ctx, mqttMsg.variableHeader().packetId(), otaPackageId, requestId, chunkSize, chunk, type); | ||
480 | + } else { | ||
481 | + TransportProtos.SessionInfoProto sessionInfo = deviceSessionCtx.getSessionInfo(); | ||
482 | + TransportProtos.GetOtaPackageRequestMsg getOtaPackageRequestMsg = TransportProtos.GetOtaPackageRequestMsg.newBuilder() | ||
483 | + .setDeviceIdMSB(sessionInfo.getDeviceIdMSB()) | ||
484 | + .setDeviceIdLSB(sessionInfo.getDeviceIdLSB()) | ||
485 | + .setTenantIdMSB(sessionInfo.getTenantIdMSB()) | ||
486 | + .setTenantIdLSB(sessionInfo.getTenantIdLSB()) | ||
487 | + .setType(type.name()) | ||
488 | + .build(); | ||
489 | + transportService.process(deviceSessionCtx.getSessionInfo(), getOtaPackageRequestMsg, | ||
490 | + new OtaPackageCallback(ctx, msgId, getOtaPackageRequestMsg, requestId, chunkSize, chunk)); | ||
491 | + } | ||
492 | + } | ||
493 | + | ||
494 | + private void ack(ChannelHandlerContext ctx, int msgId) { | ||
495 | + if (msgId > 0) { | ||
496 | + ctx.writeAndFlush(createMqttPubAckMsg(msgId)); | ||
497 | + } | ||
498 | + } | ||
499 | + | ||
500 | + private <T> TransportServiceCallback<Void> getPubAckCallback(final ChannelHandlerContext ctx, final int msgId, final T msg) { | ||
501 | + return new TransportServiceCallback<>() { | ||
502 | + @Override | ||
503 | + public void onSuccess(Void dummy) { | ||
504 | + log.trace("[{}] Published msg: {}", sessionId, msg); | ||
505 | + ack(ctx, msgId); | ||
506 | + } | ||
507 | + | ||
508 | + @Override | ||
509 | + public void onError(Throwable e) { | ||
510 | + log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); | ||
511 | + ctx.close(); | ||
512 | + } | ||
513 | + }; | ||
514 | + } | ||
515 | + | ||
516 | + private class DeviceProvisionCallback implements TransportServiceCallback<ProvisionDeviceResponseMsg> { | ||
517 | + private final ChannelHandlerContext ctx; | ||
518 | + private final int msgId; | ||
519 | + private final TransportProtos.ProvisionDeviceRequestMsg msg; | ||
520 | + | ||
521 | + DeviceProvisionCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.ProvisionDeviceRequestMsg msg) { | ||
522 | + this.ctx = ctx; | ||
523 | + this.msgId = msgId; | ||
524 | + this.msg = msg; | ||
525 | + } | ||
526 | + | ||
527 | + @Override | ||
528 | + public void onSuccess(TransportProtos.ProvisionDeviceResponseMsg provisionResponseMsg) { | ||
529 | + log.trace("[{}] Published msg: {}", sessionId, msg); | ||
530 | + ack(ctx, msgId); | ||
531 | + try { | ||
532 | + if (deviceSessionCtx.getProvisionPayloadType().equals(TransportPayloadType.JSON)) { | ||
533 | + deviceSessionCtx.getContext().getJsonMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
534 | + } else { | ||
535 | + deviceSessionCtx.getContext().getProtoMqttAdaptor().convertToPublish(deviceSessionCtx, provisionResponseMsg).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
536 | + } | ||
537 | + scheduler.schedule((Callable<ChannelFuture>) ctx::close, 60, TimeUnit.SECONDS); | ||
538 | + } catch (Exception e) { | ||
539 | + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); | ||
540 | + } | ||
541 | + } | ||
542 | + | ||
543 | + @Override | ||
544 | + public void onError(Throwable e) { | ||
545 | + log.trace("[{}] Failed to publish msg: {}", sessionId, msg, e); | ||
546 | + ctx.close(); | ||
547 | + } | ||
548 | + } | ||
549 | + | ||
550 | + private class OtaPackageCallback implements TransportServiceCallback<TransportProtos.GetOtaPackageResponseMsg> { | ||
551 | + private final ChannelHandlerContext ctx; | ||
552 | + private final int msgId; | ||
553 | + private final TransportProtos.GetOtaPackageRequestMsg msg; | ||
554 | + private final String requestId; | ||
555 | + private final int chunkSize; | ||
556 | + private final int chunk; | ||
557 | + | ||
558 | + OtaPackageCallback(ChannelHandlerContext ctx, int msgId, TransportProtos.GetOtaPackageRequestMsg msg, String requestId, int chunkSize, int chunk) { | ||
559 | + this.ctx = ctx; | ||
560 | + this.msgId = msgId; | ||
561 | + this.msg = msg; | ||
562 | + this.requestId = requestId; | ||
563 | + this.chunkSize = chunkSize; | ||
564 | + this.chunk = chunk; | ||
565 | + } | ||
566 | + | ||
567 | + @Override | ||
568 | + public void onSuccess(TransportProtos.GetOtaPackageResponseMsg response) { | ||
569 | + if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { | ||
570 | + OtaPackageId firmwareId = new OtaPackageId(new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB())); | ||
571 | + otaPackSessions.put(requestId, firmwareId.toString()); | ||
572 | + sendOtaPackage(ctx, msgId, firmwareId.toString(), requestId, chunkSize, chunk, OtaPackageType.valueOf(response.getType())); | ||
573 | + } else { | ||
574 | + sendOtaPackageError(ctx, response.getResponseStatus().toString()); | ||
575 | + } | ||
576 | + } | ||
577 | + | ||
578 | + @Override | ||
579 | + public void onError(Throwable e) { | ||
580 | + log.trace("[{}] Failed to get firmware: {}", sessionId, msg, e); | ||
581 | + ctx.close(); | ||
582 | + } | ||
583 | + } | ||
584 | + | ||
585 | + private void sendOtaPackage(ChannelHandlerContext ctx, int msgId, String firmwareId, String requestId, int chunkSize, int chunk, OtaPackageType type) { | ||
586 | + log.trace("[{}] Send firmware [{}] to device!", sessionId, firmwareId); | ||
587 | + ack(ctx, msgId); | ||
588 | + try { | ||
589 | + byte[] firmwareChunk = context.getOtaPackageDataCache().get(firmwareId, chunkSize, chunk); | ||
590 | + deviceSessionCtx.getPayloadAdaptor() | ||
591 | + .convertToPublish(deviceSessionCtx, firmwareChunk, requestId, chunk, type) | ||
592 | + .ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
593 | + } catch (Exception e) { | ||
594 | + log.trace("[{}] Failed to send firmware response!", sessionId, e); | ||
595 | + } | ||
596 | + } | ||
597 | + | ||
598 | + private void sendOtaPackageError(ChannelHandlerContext ctx, String error) { | ||
599 | + log.warn("[{}] {}", sessionId, error); | ||
600 | + deviceSessionCtx.getChannel().writeAndFlush(deviceSessionCtx | ||
601 | + .getPayloadAdaptor() | ||
602 | + .createMqttPublishMsg(deviceSessionCtx, MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC, error.getBytes())); | ||
603 | + ctx.close(); | ||
604 | + } | ||
605 | + | ||
606 | + private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) { | ||
607 | + if (!checkConnected(ctx, mqttMsg)) { | ||
608 | + return; | ||
609 | + } | ||
610 | + log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId()); | ||
611 | + List<Integer> grantedQoSList = new ArrayList<>(); | ||
612 | + boolean activityReported = false; | ||
613 | + for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) { | ||
614 | + String topic = subscription.topicName(); | ||
615 | + MqttQoS reqQoS = subscription.qualityOfService(); | ||
616 | + try { | ||
617 | + switch (topic) { | ||
618 | + case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { | ||
619 | + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); | ||
620 | + activityReported = true; | ||
621 | + break; | ||
622 | + } | ||
623 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { | ||
624 | + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); | ||
625 | + activityReported = true; | ||
626 | + break; | ||
627 | + } | ||
628 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { | ||
629 | + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); | ||
630 | + activityReported = true; | ||
631 | + break; | ||
632 | + } | ||
633 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { | ||
634 | + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); | ||
635 | + activityReported = true; | ||
636 | + break; | ||
637 | + } | ||
638 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { | ||
639 | + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); | ||
640 | + activityReported = true; | ||
641 | + break; | ||
642 | + } | ||
643 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { | ||
644 | + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); | ||
645 | + activityReported = true; | ||
646 | + break; | ||
647 | + } | ||
648 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { | ||
649 | + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); | ||
650 | + activityReported = true; | ||
651 | + break; | ||
652 | + } | ||
653 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { | ||
654 | + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); | ||
655 | + activityReported = true; | ||
656 | + break; | ||
657 | + } | ||
658 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: | ||
659 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: | ||
660 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: | ||
661 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: | ||
662 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: | ||
663 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: | ||
664 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: | ||
665 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: | ||
666 | + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: | ||
667 | + case MqttTopics.GATEWAY_RPC_TOPIC: | ||
668 | + case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: | ||
669 | + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: | ||
670 | + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: | ||
671 | + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: | ||
672 | + case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: | ||
673 | + case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: | ||
674 | + registerSubQoS(topic, grantedQoSList, reqQoS); | ||
675 | + break; | ||
676 | + default: | ||
677 | + log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); | ||
678 | + grantedQoSList.add(FAILURE.value()); | ||
679 | + break; | ||
680 | + } | ||
681 | + } catch (Exception e) { | ||
682 | + log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS, e); | ||
683 | + grantedQoSList.add(FAILURE.value()); | ||
684 | + } | ||
685 | + } | ||
686 | + if (!activityReported) { | ||
687 | + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); | ||
688 | + } | ||
689 | + ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList)); | ||
690 | + } | ||
691 | + | ||
692 | + private void processRpcSubscribe(List<Integer> grantedQoSList, String topic, MqttQoS reqQoS, TopicType topicType) { | ||
693 | + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); | ||
694 | + rpcSubTopicType = topicType; | ||
695 | + registerSubQoS(topic, grantedQoSList, reqQoS); | ||
696 | + } | ||
697 | + | ||
698 | + private void processAttributesSubscribe(List<Integer> grantedQoSList, String topic, MqttQoS reqQoS, TopicType topicType) { | ||
699 | + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); | ||
700 | + attrSubTopicType = topicType; | ||
701 | + registerSubQoS(topic, grantedQoSList, reqQoS); | ||
702 | + } | ||
703 | + | ||
704 | + private void registerSubQoS(String topic, List<Integer> grantedQoSList, MqttQoS reqQoS) { | ||
705 | + grantedQoSList.add(getMinSupportedQos(reqQoS)); | ||
706 | + mqttQoSMap.put(new MqttTopicMatcher(topic), getMinSupportedQos(reqQoS)); | ||
707 | + } | ||
708 | + | ||
709 | + private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) { | ||
710 | + if (!checkConnected(ctx, mqttMsg)) { | ||
711 | + return; | ||
712 | + } | ||
713 | + boolean activityReported = false; | ||
714 | + log.trace("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId()); | ||
715 | + for (String topicName : mqttMsg.payload().topics()) { | ||
716 | + mqttQoSMap.remove(new MqttTopicMatcher(topicName)); | ||
717 | + try { | ||
718 | + switch (topicName) { | ||
719 | + case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: | ||
720 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: | ||
721 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: | ||
722 | + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { | ||
723 | + transportService.process(deviceSessionCtx.getSessionInfo(), | ||
724 | + TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); | ||
725 | + activityReported = true; | ||
726 | + break; | ||
727 | + } | ||
728 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: | ||
729 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: | ||
730 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: | ||
731 | + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { | ||
732 | + transportService.process(deviceSessionCtx.getSessionInfo(), | ||
733 | + TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); | ||
734 | + activityReported = true; | ||
735 | + break; | ||
736 | + } | ||
737 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: | ||
738 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: | ||
739 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: | ||
740 | + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: | ||
741 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: | ||
742 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: | ||
743 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: | ||
744 | + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: | ||
745 | + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: | ||
746 | + case MqttTopics.GATEWAY_RPC_TOPIC: | ||
747 | + case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: | ||
748 | + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: | ||
749 | + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: | ||
750 | + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: | ||
751 | + case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: | ||
752 | + case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: { | ||
753 | + activityReported = true; | ||
754 | + break; | ||
755 | + } | ||
756 | + } | ||
757 | + } catch (Exception e) { | ||
758 | + log.debug("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName); | ||
759 | + } | ||
760 | + } | ||
761 | + if (!activityReported) { | ||
762 | + transportService.reportActivity(deviceSessionCtx.getSessionInfo()); | ||
763 | + } | ||
764 | + ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId())); | ||
765 | + } | ||
766 | + | ||
767 | + private MqttMessage createUnSubAckMessage(int msgId) { | ||
768 | + MqttFixedHeader mqttFixedHeader = | ||
769 | + new MqttFixedHeader(UNSUBACK, false, AT_MOST_ONCE, false, 0); | ||
770 | + MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); | ||
771 | + return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); | ||
772 | + } | ||
773 | + | ||
774 | + void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | ||
775 | + log.debug("[{}][{}] Processing connect msg for client: {}!", address, sessionId, msg.payload().clientIdentifier()); | ||
776 | + String userName = msg.payload().userName(); | ||
777 | + String clientId = msg.payload().clientIdentifier(); | ||
778 | + if (DataConstants.PROVISION.equals(userName) || DataConstants.PROVISION.equals(clientId)) { | ||
779 | + deviceSessionCtx.setProvisionOnly(true); | ||
780 | + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, msg)); | ||
781 | + } else { | ||
782 | + X509Certificate cert; | ||
783 | + if (sslHandler != null && (cert = getX509Certificate()) != null) { | ||
784 | + processX509CertConnect(ctx, cert, msg); | ||
785 | + } else { | ||
786 | + processAuthTokenConnect(ctx, msg); | ||
787 | + } | ||
788 | + } | ||
789 | + } | ||
790 | + | ||
791 | + private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage connectMessage) { | ||
792 | + String userName = connectMessage.payload().userName(); | ||
793 | + log.debug("[{}][{}] Processing connect msg for client with user name: {}!", address, sessionId, userName); | ||
794 | + TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder() | ||
795 | + .setClientId(connectMessage.payload().clientIdentifier()); | ||
796 | + if (userName != null) { | ||
797 | + request.setUserName(userName); | ||
798 | + } | ||
799 | + byte[] passwordBytes = connectMessage.payload().passwordInBytes(); | ||
800 | + if (passwordBytes != null) { | ||
801 | + String password = new String(passwordBytes, CharsetUtil.UTF_8); | ||
802 | + request.setPassword(password); | ||
803 | + } | ||
804 | + transportService.process(DeviceTransportType.MQTT, request.build(), | ||
805 | + new TransportServiceCallback<>() { | ||
806 | + @Override | ||
807 | + public void onSuccess(ValidateDeviceCredentialsResponse msg) { | ||
808 | + onValidateDeviceResponse(msg, ctx, connectMessage); | ||
809 | + } | ||
810 | + | ||
811 | + @Override | ||
812 | + public void onError(Throwable e) { | ||
813 | + log.trace("[{}] Failed to process credentials: {}", address, userName, e); | ||
814 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage)); | ||
815 | + ctx.close(); | ||
816 | + } | ||
817 | + }); | ||
818 | + } | ||
819 | + | ||
820 | + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert, MqttConnectMessage connectMessage) { | ||
821 | + try { | ||
822 | + if (!context.isSkipValidityCheckForClientCert()) { | ||
823 | + cert.checkValidity(); | ||
824 | + } | ||
825 | + String strCert = SslUtil.getCertificateString(cert); | ||
826 | + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); | ||
827 | + transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), | ||
828 | + new TransportServiceCallback<>() { | ||
829 | + @Override | ||
830 | + public void onSuccess(ValidateDeviceCredentialsResponse msg) { | ||
831 | + onValidateDeviceResponse(msg, ctx, connectMessage); | ||
832 | + } | ||
833 | + | ||
834 | + @Override | ||
835 | + public void onError(Throwable e) { | ||
836 | + log.trace("[{}] Failed to process credentials: {}", address, sha3Hash, e); | ||
837 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage)); | ||
838 | + ctx.close(); | ||
839 | + } | ||
840 | + }); | ||
841 | + } catch (Exception e) { | ||
842 | + context.onAuthFailure(address); | ||
843 | + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED, connectMessage)); | ||
844 | + log.trace("[{}] X509 auth failure: {}", sessionId, address, e); | ||
845 | + ctx.close(); | ||
846 | + } | ||
847 | + } | ||
848 | + | ||
849 | + private X509Certificate getX509Certificate() { | ||
850 | + try { | ||
851 | + Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificates(); | ||
852 | + if (certChain.length > 0) { | ||
853 | + return (X509Certificate) certChain[0]; | ||
854 | + } | ||
855 | + } catch (SSLPeerUnverifiedException e) { | ||
856 | + log.warn(e.getMessage()); | ||
857 | + return null; | ||
858 | + } | ||
859 | + return null; | ||
860 | + } | ||
861 | + | ||
862 | + private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode, MqttConnectMessage msg) { | ||
863 | + MqttFixedHeader mqttFixedHeader = | ||
864 | + new MqttFixedHeader(CONNACK, false, AT_MOST_ONCE, false, 0); | ||
865 | + MqttConnAckVariableHeader mqttConnAckVariableHeader = | ||
866 | + new MqttConnAckVariableHeader(returnCode, !msg.variableHeader().isCleanSession()); | ||
867 | + return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader); | ||
868 | + } | ||
869 | + | ||
870 | + @Override | ||
871 | + public void channelReadComplete(ChannelHandlerContext ctx) { | ||
872 | + ctx.flush(); | ||
873 | + } | ||
874 | + | ||
875 | + @Override | ||
876 | + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { | ||
877 | + log.error("[{}] Unexpected Exception", sessionId, cause); | ||
878 | + ctx.close(); | ||
879 | + if (cause instanceof OutOfMemoryError) { | ||
880 | + log.error("Received critical error. Going to shutdown the service."); | ||
881 | + System.exit(1); | ||
882 | + } | ||
883 | + } | ||
884 | + | ||
885 | + private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) { | ||
886 | + MqttFixedHeader mqttFixedHeader = | ||
887 | + new MqttFixedHeader(SUBACK, false, AT_MOST_ONCE, false, 0); | ||
888 | + MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId); | ||
889 | + MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList); | ||
890 | + return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload); | ||
891 | + } | ||
892 | + | ||
893 | + private static int getMinSupportedQos(MqttQoS reqQoS) { | ||
894 | + return Math.min(reqQoS.value(), MAX_SUPPORTED_QOS_LVL.value()); | ||
895 | + } | ||
896 | + | ||
897 | + public static MqttPubAckMessage createMqttPubAckMsg(int requestId) { | ||
898 | + MqttFixedHeader mqttFixedHeader = | ||
899 | + new MqttFixedHeader(PUBACK, false, AT_MOST_ONCE, false, 0); | ||
900 | + MqttMessageIdVariableHeader mqttMsgIdVariableHeader = | ||
901 | + MqttMessageIdVariableHeader.from(requestId); | ||
902 | + return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader); | ||
903 | + } | ||
904 | + | ||
905 | + private boolean checkConnected(ChannelHandlerContext ctx, MqttMessage msg) { | ||
906 | + if (deviceSessionCtx.isConnected()) { | ||
907 | + return true; | ||
908 | + } else { | ||
909 | + log.info("[{}] Closing current session due to invalid msg order: {}", sessionId, msg); | ||
910 | + return false; | ||
911 | + } | ||
912 | + } | ||
913 | + | ||
914 | + private void checkGatewaySession(SessionMetaData sessionMetaData) { | ||
915 | + TransportDeviceInfo device = deviceSessionCtx.getDeviceInfo(); | ||
916 | + try { | ||
917 | + JsonNode infoNode = context.getMapper().readTree(device.getAdditionalInfo()); | ||
918 | + if (infoNode != null) { | ||
919 | + JsonNode gatewayNode = infoNode.get("gateway"); | ||
920 | + if (gatewayNode != null && gatewayNode.asBoolean()) { | ||
921 | + gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId); | ||
922 | + if (infoNode.has(DefaultTransportService.OVERWRITE_ACTIVITY_TIME) && infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).isBoolean()) { | ||
923 | + sessionMetaData.setOverwriteActivityTime(infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).asBoolean()); | ||
924 | + } | ||
925 | + } | ||
926 | + } | ||
927 | + } catch (IOException e) { | ||
928 | + log.trace("[{}][{}] Failed to fetch device additional info", sessionId, device.getDeviceName(), e); | ||
929 | + } | ||
930 | + } | ||
931 | + | ||
932 | + @Override | ||
933 | + public void operationComplete(Future<? super Void> future) throws Exception { | ||
934 | + log.trace("[{}] Channel closed!", sessionId); | ||
935 | + doDisconnect(); | ||
936 | + } | ||
937 | + | ||
938 | + public void doDisconnect() { | ||
939 | + if (deviceSessionCtx.isConnected()) { | ||
940 | + log.debug("[{}] Client disconnected!", sessionId); | ||
941 | + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); | ||
942 | + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); | ||
943 | + if (gatewaySessionHandler != null) { | ||
944 | + gatewaySessionHandler.onGatewayDisconnect(); | ||
945 | + } | ||
946 | + deviceSessionCtx.setDisconnected(); | ||
947 | + } | ||
948 | + deviceSessionCtx.release(); | ||
949 | + } | ||
950 | + | ||
951 | + | ||
952 | + private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx, MqttConnectMessage connectMessage) { | ||
953 | + if (!msg.hasDeviceInfo()) { | ||
954 | + context.onAuthFailure(address); | ||
955 | + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED, connectMessage)); | ||
956 | + ctx.close(); | ||
957 | + } else { | ||
958 | + context.onAuthSuccess(address); | ||
959 | + deviceSessionCtx.setDeviceInfo(msg.getDeviceInfo()); | ||
960 | + deviceSessionCtx.setDeviceProfile(msg.getDeviceProfile()); | ||
961 | + deviceSessionCtx.setSessionInfo(SessionInfoCreator.create(msg, context, sessionId)); | ||
962 | + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_OPEN, new TransportServiceCallback<Void>() { | ||
963 | + @Override | ||
964 | + public void onSuccess(Void msg) { | ||
965 | + SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), TcpTransportHandler.this); | ||
966 | + checkGatewaySession(sessionMetaData); | ||
967 | + ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); | ||
968 | + deviceSessionCtx.setConnected(true); | ||
969 | + log.debug("[{}] Client connected!", sessionId); | ||
970 | + transportService.getCallbackExecutor().execute(() -> processMsgQueue(ctx)); //this callback will execute in Producer worker thread and hard or blocking work have to be submitted to the separate thread. | ||
971 | + } | ||
972 | + | ||
973 | + @Override | ||
974 | + public void onError(Throwable e) { | ||
975 | + if (e instanceof TbRateLimitsException) { | ||
976 | + log.trace("[{}] Failed to submit session event: {}", sessionId, e.getMessage()); | ||
977 | + } else { | ||
978 | + log.warn("[{}] Failed to submit session event", sessionId, e); | ||
979 | + } | ||
980 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE, connectMessage)); | ||
981 | + ctx.close(); | ||
982 | + } | ||
983 | + }); | ||
984 | + } | ||
985 | + } | ||
986 | + | ||
987 | + @Override | ||
988 | + public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { | ||
989 | + log.trace("[{}] Received get attributes response", sessionId); | ||
990 | + String topicBase = attrReqTopicType.getAttributesResponseTopicBase(); | ||
991 | + TcpTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrReqTopicType); | ||
992 | + try { | ||
993 | + adaptor.convertToPublish(deviceSessionCtx, response, topicBase).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
994 | + } catch (Exception e) { | ||
995 | + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); | ||
996 | + } | ||
997 | + } | ||
998 | + | ||
999 | + @Override | ||
1000 | + public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { | ||
1001 | + log.trace("[{}] Received attributes update notification to device", sessionId); | ||
1002 | + String topic = attrSubTopicType.getAttributesSubTopic(); | ||
1003 | + TcpTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); | ||
1004 | + try { | ||
1005 | + adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
1006 | + } catch (Exception e) { | ||
1007 | + log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); | ||
1008 | + } | ||
1009 | + } | ||
1010 | + | ||
1011 | + @Override | ||
1012 | + public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { | ||
1013 | + log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); | ||
1014 | + deviceSessionCtx.getChannel().close(); | ||
1015 | + } | ||
1016 | + | ||
1017 | + @Override | ||
1018 | + public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { | ||
1019 | + log.trace("[{}] Received RPC command to device", sessionId); | ||
1020 | + String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); | ||
1021 | + TcpTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); | ||
1022 | + try { | ||
1023 | + adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic).ifPresent(payload -> { | ||
1024 | + int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); | ||
1025 | + if (isAckExpected(payload)) { | ||
1026 | + rpcAwaitingAck.put(msgId, rpcRequest); | ||
1027 | + context.getScheduler().schedule(() -> { | ||
1028 | + TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); | ||
1029 | + if (msg != null) { | ||
1030 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); | ||
1031 | + } | ||
1032 | + }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); | ||
1033 | + } | ||
1034 | + var cf = publish(payload, deviceSessionCtx); | ||
1035 | + cf.addListener(result -> { | ||
1036 | + if (result.cause() == null) { | ||
1037 | + if (!isAckExpected(payload)) { | ||
1038 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); | ||
1039 | + } else if (rpcRequest.getPersisted()) { | ||
1040 | + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); | ||
1041 | + } | ||
1042 | + } else { | ||
1043 | + // TODO: send error | ||
1044 | + } | ||
1045 | + }); | ||
1046 | + }); | ||
1047 | + } catch (Exception e) { | ||
1048 | + transportService.process(deviceSessionCtx.getSessionInfo(), | ||
1049 | + TransportProtos.ToDeviceRpcResponseMsg.newBuilder() | ||
1050 | + .setRequestId(rpcRequest.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY); | ||
1051 | + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); | ||
1052 | + } | ||
1053 | + } | ||
1054 | + | ||
1055 | + @Override | ||
1056 | + public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { | ||
1057 | + log.trace("[{}] Received RPC response from server", sessionId); | ||
1058 | + String baseTopic = toServerRpcSubTopicType.getRpcResponseTopicBase(); | ||
1059 | + TcpTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(toServerRpcSubTopicType); | ||
1060 | + try { | ||
1061 | + adaptor.convertToPublish(deviceSessionCtx, rpcResponse, baseTopic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); | ||
1062 | + } catch (Exception e) { | ||
1063 | + log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); | ||
1064 | + } | ||
1065 | + } | ||
1066 | + | ||
1067 | + private ChannelFuture publish(MqttMessage message, DeviceSessionCtx deviceSessionCtx) { | ||
1068 | + return deviceSessionCtx.getChannel().writeAndFlush(message); | ||
1069 | + } | ||
1070 | + | ||
1071 | + private boolean isAckExpected(MqttMessage message) { | ||
1072 | + return message.fixedHeader().qosLevel().value() > 0; | ||
1073 | + } | ||
1074 | + | ||
1075 | + @Override | ||
1076 | + public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { | ||
1077 | + deviceSessionCtx.onDeviceProfileUpdate(sessionInfo, deviceProfile); | ||
1078 | + } | ||
1079 | + | ||
1080 | + @Override | ||
1081 | + public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) { | ||
1082 | + deviceSessionCtx.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); | ||
1083 | + } | ||
1084 | + | ||
1085 | + @Override | ||
1086 | + public void onDeviceDeleted(DeviceId deviceId) { | ||
1087 | + context.onAuthFailure(address); | ||
1088 | + ChannelHandlerContext ctx = deviceSessionCtx.getChannel(); | ||
1089 | + ctx.close(); | ||
1090 | + } | ||
1091 | + | ||
1092 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import io.netty.channel.ChannelInitializer; | ||
19 | +import io.netty.channel.ChannelPipeline; | ||
20 | +import io.netty.channel.socket.SocketChannel; | ||
21 | +import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; | ||
22 | +import io.netty.handler.codec.mqtt.MqttDecoder; | ||
23 | +import io.netty.handler.codec.mqtt.MqttEncoder; | ||
24 | +import io.netty.handler.ssl.SslHandler; | ||
25 | +import org.thingsboard.server.transport.tcp.limits.IpFilter; | ||
26 | +import org.thingsboard.server.transport.tcp.limits.ProxyIpFilter; | ||
27 | + | ||
28 | +/** | ||
29 | + * @author Andrew Shvayka | ||
30 | + */ | ||
31 | +public class TcpTransportServerInitializer extends ChannelInitializer<SocketChannel> { | ||
32 | + | ||
33 | + private final TcpTransportContext context; | ||
34 | + private final boolean sslEnabled; | ||
35 | + | ||
36 | + public TcpTransportServerInitializer(TcpTransportContext context, boolean sslEnabled) { | ||
37 | + this.context = context; | ||
38 | + this.sslEnabled = sslEnabled; | ||
39 | + } | ||
40 | + | ||
41 | + @Override | ||
42 | + public void initChannel(SocketChannel ch) { | ||
43 | + ChannelPipeline pipeline = ch.pipeline(); | ||
44 | + SslHandler sslHandler = null; | ||
45 | + if (context.isProxyEnabled()) { | ||
46 | + pipeline.addLast("proxy", new HAProxyMessageDecoder()); | ||
47 | + pipeline.addLast("ipFilter", new ProxyIpFilter(context)); | ||
48 | + } else { | ||
49 | + pipeline.addLast("ipFilter", new IpFilter(context)); | ||
50 | + } | ||
51 | + if (sslEnabled && context.getSslHandlerProvider() != null) { | ||
52 | + sslHandler = context.getSslHandlerProvider().getSslHandler(); | ||
53 | + pipeline.addLast(sslHandler); | ||
54 | + } | ||
55 | + pipeline.addLast("decoder", new MqttDecoder(context.getMaxPayloadSize())); | ||
56 | + pipeline.addLast("encoder", MqttEncoder.INSTANCE); | ||
57 | + | ||
58 | + TcpTransportHandler handler = new TcpTransportHandler(context, sslHandler); | ||
59 | + | ||
60 | + pipeline.addLast(handler); | ||
61 | + ch.closeFuture().addListener(handler); | ||
62 | + } | ||
63 | + | ||
64 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/TcpTransportService.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import io.netty.bootstrap.ServerBootstrap; | ||
19 | +import io.netty.channel.Channel; | ||
20 | +import io.netty.channel.ChannelOption; | ||
21 | +import io.netty.channel.EventLoopGroup; | ||
22 | +import io.netty.channel.nio.NioEventLoopGroup; | ||
23 | +import io.netty.channel.socket.nio.NioServerSocketChannel; | ||
24 | +import io.netty.util.AttributeKey; | ||
25 | +import io.netty.util.ResourceLeakDetector; | ||
26 | +import lombok.extern.slf4j.Slf4j; | ||
27 | +import org.springframework.beans.factory.annotation.Autowired; | ||
28 | +import org.springframework.beans.factory.annotation.Value; | ||
29 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | ||
30 | +import org.springframework.stereotype.Service; | ||
31 | +import org.thingsboard.server.common.data.DataConstants; | ||
32 | +import org.thingsboard.server.common.data.TbTransportService; | ||
33 | + | ||
34 | +import javax.annotation.PostConstruct; | ||
35 | +import javax.annotation.PreDestroy; | ||
36 | +import java.net.InetSocketAddress; | ||
37 | + | ||
38 | +/** | ||
39 | + * @author Andrew Shvayka | ||
40 | + */ | ||
41 | +@Service("TcpTransportService") | ||
42 | +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.tcp.enabled}'=='true')") | ||
43 | +@Slf4j | ||
44 | +public class TcpTransportService implements TbTransportService { | ||
45 | + | ||
46 | + public static AttributeKey<InetSocketAddress> ADDRESS = AttributeKey.newInstance("SRC_ADDRESS"); | ||
47 | + | ||
48 | + @Value("${transport.tcp.bind_address}") | ||
49 | + private String host; | ||
50 | + @Value("${transport.tcp.bind_port}") | ||
51 | + private Integer port; | ||
52 | + | ||
53 | + @Value("${transport.tcp.ssl.enabled}") | ||
54 | + private boolean sslEnabled; | ||
55 | + | ||
56 | + @Value("${transport.tcp.ssl.bind_address}") | ||
57 | + private String sslHost; | ||
58 | + @Value("${transport.tcp.ssl.bind_port}") | ||
59 | + private Integer sslPort; | ||
60 | + | ||
61 | + @Value("${transport.tcp.netty.leak_detector_level}") | ||
62 | + private String leakDetectorLevel; | ||
63 | + @Value("${transport.tcp.netty.boss_group_thread_count}") | ||
64 | + private Integer bossGroupThreadCount; | ||
65 | + @Value("${transport.tcp.netty.worker_group_thread_count}") | ||
66 | + private Integer workerGroupThreadCount; | ||
67 | + @Value("${transport.tcp.netty.so_keep_alive}") | ||
68 | + private boolean keepAlive; | ||
69 | + | ||
70 | + @Autowired | ||
71 | + private TcpTransportContext context; | ||
72 | + | ||
73 | + private Channel serverChannel; | ||
74 | + private Channel sslServerChannel; | ||
75 | + private EventLoopGroup bossGroup; | ||
76 | + private EventLoopGroup workerGroup; | ||
77 | + | ||
78 | + @PostConstruct | ||
79 | + public void init() throws Exception { | ||
80 | + log.info("Setting resource leak detector level to {}", leakDetectorLevel); | ||
81 | + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(leakDetectorLevel.toUpperCase())); | ||
82 | + | ||
83 | + log.info("Starting TCP transport..."); | ||
84 | + bossGroup = new NioEventLoopGroup(bossGroupThreadCount); | ||
85 | + workerGroup = new NioEventLoopGroup(workerGroupThreadCount); | ||
86 | + ServerBootstrap b = new ServerBootstrap(); | ||
87 | + b.group(bossGroup, workerGroup) | ||
88 | + .channel(NioServerSocketChannel.class) | ||
89 | + .childHandler(new TcpTransportServerInitializer(context, false)) | ||
90 | + .childOption(ChannelOption.SO_KEEPALIVE, keepAlive); | ||
91 | + | ||
92 | + serverChannel = b.bind(host, port).sync().channel(); | ||
93 | + if (sslEnabled) { | ||
94 | + b = new ServerBootstrap(); | ||
95 | + b.group(bossGroup, workerGroup) | ||
96 | + .channel(NioServerSocketChannel.class) | ||
97 | + .childHandler(new TcpTransportServerInitializer(context, true)) | ||
98 | + .childOption(ChannelOption.SO_KEEPALIVE, keepAlive); | ||
99 | + sslServerChannel = b.bind(sslHost, sslPort).sync().channel(); | ||
100 | + } | ||
101 | + log.info("TCP transport started!"); | ||
102 | + } | ||
103 | + | ||
104 | + @PreDestroy | ||
105 | + public void shutdown() throws InterruptedException { | ||
106 | + log.info("Stopping TCP transport!"); | ||
107 | + try { | ||
108 | + serverChannel.close().sync(); | ||
109 | + if (sslEnabled) { | ||
110 | + sslServerChannel.close().sync(); | ||
111 | + } | ||
112 | + } finally { | ||
113 | + workerGroup.shutdownGracefully(); | ||
114 | + bossGroup.shutdownGracefully(); | ||
115 | + } | ||
116 | + log.info("TCP transport stopped!"); | ||
117 | + } | ||
118 | + | ||
119 | + @Override | ||
120 | + public String getName() { | ||
121 | + return DataConstants.TCP_TRANSPORT_NAME; | ||
122 | + } | ||
123 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp; | ||
17 | + | ||
18 | +import lombok.Getter; | ||
19 | +import org.thingsboard.server.common.data.device.profile.MqttTopics; | ||
20 | + | ||
21 | +public enum TopicType { | ||
22 | + | ||
23 | + V1(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC), | ||
24 | + V2(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC), | ||
25 | + V2_JSON(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_JSON_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC), | ||
26 | + V2_PROTO(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_PROTO_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC); | ||
27 | + | ||
28 | + @Getter | ||
29 | + private final String attributesResponseTopicBase; | ||
30 | + | ||
31 | + @Getter | ||
32 | + private final String attributesSubTopic; | ||
33 | + | ||
34 | + @Getter | ||
35 | + private final String rpcRequestTopicBase; | ||
36 | + | ||
37 | + @Getter | ||
38 | + private final String rpcResponseTopicBase; | ||
39 | + | ||
40 | + TopicType(String attributesRequestTopicBase, String attributesSubTopic, String rpcRequestTopicBase, String rpcResponseTopicBase) { | ||
41 | + this.attributesResponseTopicBase = attributesRequestTopicBase; | ||
42 | + this.attributesSubTopic = attributesSubTopic; | ||
43 | + this.rpcRequestTopicBase = rpcRequestTopicBase; | ||
44 | + this.rpcResponseTopicBase = rpcResponseTopicBase; | ||
45 | + } | ||
46 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.adaptors; | ||
17 | + | ||
18 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
19 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
20 | +import lombok.AllArgsConstructor; | ||
21 | +import lombok.Data; | ||
22 | +import lombok.extern.slf4j.Slf4j; | ||
23 | +import org.thingsboard.server.common.data.ota.OtaPackageType; | ||
24 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
25 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
26 | +import org.thingsboard.server.transport.tcp.session.MqttDeviceAwareSessionContext; | ||
27 | + | ||
28 | +import java.util.Optional; | ||
29 | + | ||
30 | +@Data | ||
31 | +@AllArgsConstructor | ||
32 | +@Slf4j | ||
33 | +public class BackwardCompatibilityAdaptor implements TcpTransportAdaptor { | ||
34 | + | ||
35 | + private TcpTransportAdaptor protoAdaptor; | ||
36 | + private TcpTransportAdaptor jsonAdaptor; | ||
37 | + | ||
38 | + @Override | ||
39 | + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
40 | + try { | ||
41 | + return protoAdaptor.convertToPostTelemetry(ctx, inbound); | ||
42 | + } catch (AdaptorException e) { | ||
43 | + log.trace("[{}] failed to process post telemetry request msg: {} due to: ", ctx.getSessionId(), inbound, e); | ||
44 | + return jsonAdaptor.convertToPostTelemetry(ctx, inbound); | ||
45 | + } | ||
46 | + } | ||
47 | + | ||
48 | + @Override | ||
49 | + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
50 | + try { | ||
51 | + return protoAdaptor.convertToPostAttributes(ctx, inbound); | ||
52 | + } catch (AdaptorException e) { | ||
53 | + log.trace("[{}] failed to process post attributes request msg: {} due to: ", ctx.getSessionId(), inbound, e); | ||
54 | + return jsonAdaptor.convertToPostAttributes(ctx, inbound); | ||
55 | + } | ||
56 | + } | ||
57 | + | ||
58 | + @Override | ||
59 | + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
60 | + try { | ||
61 | + return protoAdaptor.convertToGetAttributes(ctx, inbound, topicBase); | ||
62 | + } catch (AdaptorException e) { | ||
63 | + log.trace("[{}] failed to process get attributes request msg: {} due to: ", ctx.getSessionId(), inbound, e); | ||
64 | + return jsonAdaptor.convertToGetAttributes(ctx, inbound, topicBase); | ||
65 | + } | ||
66 | + } | ||
67 | + | ||
68 | + @Override | ||
69 | + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { | ||
70 | + try { | ||
71 | + return protoAdaptor.convertToDeviceRpcResponse(ctx, mqttMsg, topicBase); | ||
72 | + } catch (AdaptorException e) { | ||
73 | + log.trace("[{}] failed to process to device rpc response msg: {} due to: ", ctx.getSessionId(), mqttMsg, e); | ||
74 | + return jsonAdaptor.convertToDeviceRpcResponse(ctx, mqttMsg, topicBase); | ||
75 | + } | ||
76 | + } | ||
77 | + | ||
78 | + @Override | ||
79 | + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { | ||
80 | + try { | ||
81 | + return protoAdaptor.convertToServerRpcRequest(ctx, mqttMsg, topicBase); | ||
82 | + } catch (AdaptorException e) { | ||
83 | + log.trace("[{}] failed to process to server rpc request msg: {} due to: ", ctx.getSessionId(), mqttMsg, e); | ||
84 | + return jsonAdaptor.convertToServerRpcRequest(ctx, mqttMsg, topicBase); | ||
85 | + } | ||
86 | + } | ||
87 | + | ||
88 | + @Override | ||
89 | + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
90 | + try { | ||
91 | + return protoAdaptor.convertToClaimDevice(ctx, inbound); | ||
92 | + } catch (AdaptorException e) { | ||
93 | + log.trace("[{}] failed to process claim device request msg: {} due to: ", ctx.getSessionId(), inbound, e); | ||
94 | + return jsonAdaptor.convertToClaimDevice(ctx, inbound); | ||
95 | + } | ||
96 | + } | ||
97 | + | ||
98 | + @Override | ||
99 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { | ||
100 | + log.warn("[{}] invoked not implemented adaptor method! GetAttributeResponseMsg: {} TopicBase: {}", ctx.getSessionId(), responseMsg, topicBase); | ||
101 | + return Optional.empty(); | ||
102 | + } | ||
103 | + | ||
104 | + @Override | ||
105 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { | ||
106 | + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, responseMsg); | ||
107 | + } | ||
108 | + | ||
109 | + @Override | ||
110 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) throws AdaptorException { | ||
111 | + log.warn("[{}] invoked not implemented adaptor method! AttributeUpdateNotificationMsg: {} Topic: {}", ctx.getSessionId(), notificationMsg, topic); | ||
112 | + return Optional.empty(); | ||
113 | + } | ||
114 | + | ||
115 | + @Override | ||
116 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { | ||
117 | + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, notificationMsg); | ||
118 | + } | ||
119 | + | ||
120 | + @Override | ||
121 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException { | ||
122 | + log.warn("[{}] invoked not implemented adaptor method! ToDeviceRpcRequestMsg: {} TopicBase: {}", ctx.getSessionId(), rpcRequest, topicBase); | ||
123 | + return Optional.empty(); | ||
124 | + } | ||
125 | + | ||
126 | + @Override | ||
127 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { | ||
128 | + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, rpcRequest); | ||
129 | + } | ||
130 | + | ||
131 | + @Override | ||
132 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) throws AdaptorException { | ||
133 | + log.warn("[{}] invoked not implemented adaptor method! ToServerRpcResponseMsg: {} TopicBase: {}", ctx.getSessionId(), rpcResponse, topicBase); | ||
134 | + return Optional.empty(); | ||
135 | + } | ||
136 | + | ||
137 | + @Override | ||
138 | + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
139 | + log.warn("[{}] invoked not implemented adaptor method! MqttPublishMessage: {}", ctx.getSessionId(), inbound); | ||
140 | + return null; | ||
141 | + } | ||
142 | + | ||
143 | + @Override | ||
144 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException { | ||
145 | + log.warn("[{}] invoked not implemented adaptor method! ProvisionDeviceResponseMsg: {}", ctx.getSessionId(), provisionResponse); | ||
146 | + return Optional.empty(); | ||
147 | + } | ||
148 | + | ||
149 | + @Override | ||
150 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException { | ||
151 | + return protoAdaptor.convertToPublish(ctx, firmwareChunk, requestId, chunk, firmwareType); | ||
152 | + } | ||
153 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/adaptors/JsonTcpAdaptor.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.adaptors; | ||
17 | + | ||
18 | +import com.google.gson.JsonElement; | ||
19 | +import com.google.gson.JsonObject; | ||
20 | +import com.google.gson.JsonParser; | ||
21 | +import com.google.gson.JsonSyntaxException; | ||
22 | +import io.netty.buffer.ByteBuf; | ||
23 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | ||
24 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
25 | +import io.netty.handler.codec.mqtt.MqttMessageType; | ||
26 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
27 | +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; | ||
28 | +import lombok.extern.slf4j.Slf4j; | ||
29 | +import org.springframework.stereotype.Component; | ||
30 | +import org.springframework.util.StringUtils; | ||
31 | +import org.thingsboard.server.common.data.device.profile.MqttTopics; | ||
32 | +import org.thingsboard.server.common.data.ota.OtaPackageType; | ||
33 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
34 | +import org.thingsboard.server.common.transport.adaptor.JsonConverter; | ||
35 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
36 | +import org.thingsboard.server.transport.tcp.session.MqttDeviceAwareSessionContext; | ||
37 | + | ||
38 | +import java.nio.charset.Charset; | ||
39 | +import java.nio.charset.StandardCharsets; | ||
40 | +import java.util.Arrays; | ||
41 | +import java.util.HashSet; | ||
42 | +import java.util.Optional; | ||
43 | +import java.util.Set; | ||
44 | +import java.util.UUID; | ||
45 | + | ||
46 | +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; | ||
47 | + | ||
48 | + | ||
49 | +/** | ||
50 | + * @author Andrew Shvayka | ||
51 | + */ | ||
52 | +@Component | ||
53 | +@Slf4j | ||
54 | +public class JsonTcpAdaptor implements TcpTransportAdaptor { | ||
55 | + | ||
56 | + protected static final Charset UTF8 = StandardCharsets.UTF_8; | ||
57 | + | ||
58 | + @Override | ||
59 | + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
60 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | ||
61 | + try { | ||
62 | + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); | ||
63 | + } catch (IllegalStateException | JsonSyntaxException ex) { | ||
64 | + log.debug("Failed to decode post telemetry request", ex); | ||
65 | + throw new AdaptorException(ex); | ||
66 | + } | ||
67 | + } | ||
68 | + | ||
69 | + @Override | ||
70 | + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
71 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | ||
72 | + try { | ||
73 | + return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); | ||
74 | + } catch (IllegalStateException | JsonSyntaxException ex) { | ||
75 | + log.debug("Failed to decode post attributes request", ex); | ||
76 | + throw new AdaptorException(ex); | ||
77 | + } | ||
78 | + } | ||
79 | + | ||
80 | + @Override | ||
81 | + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
82 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), true); | ||
83 | + try { | ||
84 | + return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); | ||
85 | + } catch (IllegalStateException | JsonSyntaxException ex) { | ||
86 | + log.debug("Failed to decode claim device request", ex); | ||
87 | + throw new AdaptorException(ex); | ||
88 | + } | ||
89 | + } | ||
90 | + | ||
91 | + @Override | ||
92 | + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
93 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | ||
94 | + try { | ||
95 | + return JsonConverter.convertToProvisionRequestMsg(payload); | ||
96 | + } catch (IllegalStateException | JsonSyntaxException ex) { | ||
97 | + throw new AdaptorException(ex); | ||
98 | + } | ||
99 | + } | ||
100 | + | ||
101 | + @Override | ||
102 | + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
103 | + return processGetAttributeRequestMsg(inbound, topicBase); | ||
104 | + } | ||
105 | + | ||
106 | + @Override | ||
107 | + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
108 | + return processToDeviceRpcResponseMsg(inbound, topicBase); | ||
109 | + } | ||
110 | + | ||
111 | + @Override | ||
112 | + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
113 | + return processToServerRpcRequestMsg(ctx, inbound, topicBase); | ||
114 | + } | ||
115 | + | ||
116 | + @Override | ||
117 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { | ||
118 | + return processConvertFromAttributeResponseMsg(ctx, responseMsg, topicBase); | ||
119 | + } | ||
120 | + | ||
121 | + @Override | ||
122 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { | ||
123 | + return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); | ||
124 | + } | ||
125 | + | ||
126 | + @Override | ||
127 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { | ||
128 | + return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); | ||
129 | + } | ||
130 | + | ||
131 | + @Override | ||
132 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { | ||
133 | + JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, notificationMsg); | ||
134 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, result)); | ||
135 | + } | ||
136 | + | ||
137 | + @Override | ||
138 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) { | ||
139 | + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); | ||
140 | + } | ||
141 | + | ||
142 | + @Override | ||
143 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { | ||
144 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, JsonConverter.toGatewayJson(deviceName, rpcRequest))); | ||
145 | + } | ||
146 | + | ||
147 | + @Override | ||
148 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) { | ||
149 | + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); | ||
150 | + } | ||
151 | + | ||
152 | + @Override | ||
153 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { | ||
154 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, JsonConverter.toJson(provisionResponse))); | ||
155 | + } | ||
156 | + | ||
157 | + @Override | ||
158 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) { | ||
159 | + return Optional.of(createMqttPublishMsg(ctx, String.format(DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT, firmwareType.getKeyPrefix(), requestId, chunk), firmwareChunk)); | ||
160 | + } | ||
161 | + | ||
162 | + public static JsonElement validateJsonPayload(UUID sessionId, ByteBuf payloadData) throws AdaptorException { | ||
163 | + String payload = validatePayload(sessionId, payloadData, false); | ||
164 | + try { | ||
165 | + return new JsonParser().parse(payload); | ||
166 | + } catch (JsonSyntaxException ex) { | ||
167 | + log.debug("Payload is in incorrect format: {}", payload); | ||
168 | + throw new AdaptorException(ex); | ||
169 | + } | ||
170 | + } | ||
171 | + | ||
172 | + private TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
173 | + String topicName = inbound.variableHeader().topicName(); | ||
174 | + try { | ||
175 | + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); | ||
176 | + result.setRequestId(getRequestId(topicName, topicBase)); | ||
177 | + String payload = inbound.payload().toString(UTF8); | ||
178 | + JsonElement requestBody = new JsonParser().parse(payload); | ||
179 | + Set<String> clientKeys = toStringSet(requestBody, "clientKeys"); | ||
180 | + Set<String> sharedKeys = toStringSet(requestBody, "sharedKeys"); | ||
181 | + if (clientKeys != null) { | ||
182 | + result.addAllClientAttributeNames(clientKeys); | ||
183 | + } | ||
184 | + if (sharedKeys != null) { | ||
185 | + result.addAllSharedAttributeNames(sharedKeys); | ||
186 | + } | ||
187 | + return result.build(); | ||
188 | + } catch (RuntimeException e) { | ||
189 | + log.debug("Failed to decode get attributes request", e); | ||
190 | + throw new AdaptorException(e); | ||
191 | + } | ||
192 | + } | ||
193 | + | ||
194 | + private TransportProtos.ToDeviceRpcResponseMsg processToDeviceRpcResponseMsg(MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
195 | + String topicName = inbound.variableHeader().topicName(); | ||
196 | + try { | ||
197 | + int requestId = getRequestId(topicName, topicBase); | ||
198 | + String payload = inbound.payload().toString(UTF8); | ||
199 | + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(payload).build(); | ||
200 | + } catch (RuntimeException e) { | ||
201 | + log.debug("Failed to decode rpc response", e); | ||
202 | + throw new AdaptorException(e); | ||
203 | + } | ||
204 | + } | ||
205 | + | ||
206 | + private TransportProtos.ToServerRpcRequestMsg processToServerRpcRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
207 | + String topicName = inbound.variableHeader().topicName(); | ||
208 | + String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); | ||
209 | + try { | ||
210 | + int requestId = getRequestId(topicName, topicBase); | ||
211 | + return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); | ||
212 | + } catch (IllegalStateException | JsonSyntaxException ex) { | ||
213 | + log.debug("Failed to decode to server rpc request", ex); | ||
214 | + throw new AdaptorException(ex); | ||
215 | + } | ||
216 | + } | ||
217 | + | ||
218 | + private Optional<MqttMessage> processConvertFromAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { | ||
219 | + if (!StringUtils.isEmpty(responseMsg.getError())) { | ||
220 | + throw new AdaptorException(responseMsg.getError()); | ||
221 | + } else { | ||
222 | + int requestId = responseMsg.getRequestId(); | ||
223 | + if (requestId >= 0) { | ||
224 | + return Optional.of(createMqttPublishMsg(ctx, | ||
225 | + topicBase + requestId, | ||
226 | + JsonConverter.toJson(responseMsg))); | ||
227 | + } | ||
228 | + return Optional.empty(); | ||
229 | + } | ||
230 | + } | ||
231 | + | ||
232 | + private Optional<MqttMessage> processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { | ||
233 | + if (!StringUtils.isEmpty(responseMsg.getError())) { | ||
234 | + throw new AdaptorException(responseMsg.getError()); | ||
235 | + } else { | ||
236 | + JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg); | ||
237 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result)); | ||
238 | + } | ||
239 | + } | ||
240 | + | ||
241 | + protected MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, JsonElement json) { | ||
242 | + MqttFixedHeader mqttFixedHeader = | ||
243 | + new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); | ||
244 | + MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); | ||
245 | + ByteBuf payload = ALLOCATOR.buffer(); | ||
246 | + payload.writeBytes(json.toString().getBytes(UTF8)); | ||
247 | + return new MqttPublishMessage(mqttFixedHeader, header, payload); | ||
248 | + } | ||
249 | + | ||
250 | + private Set<String> toStringSet(JsonElement requestBody, String name) { | ||
251 | + JsonElement element = requestBody.getAsJsonObject().get(name); | ||
252 | + if (element != null) { | ||
253 | + return new HashSet<>(Arrays.asList(element.getAsString().split(","))); | ||
254 | + } else { | ||
255 | + return null; | ||
256 | + } | ||
257 | + } | ||
258 | + | ||
259 | + private static String validatePayload(UUID sessionId, ByteBuf payloadData, boolean isEmptyPayloadAllowed) throws AdaptorException { | ||
260 | + String payload = payloadData.toString(UTF8); | ||
261 | + if (payload == null) { | ||
262 | + log.debug("[{}] Payload is empty!", sessionId); | ||
263 | + if (!isEmptyPayloadAllowed) { | ||
264 | + throw new AdaptorException(new IllegalArgumentException("Payload is empty!")); | ||
265 | + } | ||
266 | + } | ||
267 | + return payload; | ||
268 | + } | ||
269 | + | ||
270 | + private int getRequestId(String topicName, String topic) { | ||
271 | + return Integer.parseInt(topicName.substring(topic.length())); | ||
272 | + } | ||
273 | + | ||
274 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.adaptors; | ||
17 | + | ||
18 | +import com.google.gson.JsonElement; | ||
19 | +import com.google.gson.JsonParser; | ||
20 | +import com.google.protobuf.Descriptors; | ||
21 | +import com.google.protobuf.DynamicMessage; | ||
22 | +import com.google.protobuf.InvalidProtocolBufferException; | ||
23 | +import io.netty.buffer.ByteBuf; | ||
24 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
25 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
26 | +import lombok.extern.slf4j.Slf4j; | ||
27 | +import org.springframework.stereotype.Component; | ||
28 | +import org.springframework.util.StringUtils; | ||
29 | +import org.thingsboard.server.common.data.device.profile.MqttTopics; | ||
30 | +import org.thingsboard.server.common.data.ota.OtaPackageType; | ||
31 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
32 | +import org.thingsboard.server.common.transport.adaptor.JsonConverter; | ||
33 | +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; | ||
34 | +import org.thingsboard.server.gen.transport.TransportApiProtos; | ||
35 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
36 | +import org.thingsboard.server.transport.tcp.session.DeviceSessionCtx; | ||
37 | +import org.thingsboard.server.transport.tcp.session.MqttDeviceAwareSessionContext; | ||
38 | + | ||
39 | +import java.util.Optional; | ||
40 | + | ||
41 | +import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; | ||
42 | + | ||
43 | +@Component | ||
44 | +@Slf4j | ||
45 | +public class ProtoTcpAdaptor implements TcpTransportAdaptor { | ||
46 | + | ||
47 | + @Override | ||
48 | + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
49 | + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; | ||
50 | + byte[] bytes = toBytes(inbound.payload()); | ||
51 | + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); | ||
52 | + try { | ||
53 | + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); | ||
54 | + } catch (Exception e) { | ||
55 | + log.debug("Failed to decode post telemetry request", e); | ||
56 | + throw new AdaptorException(e); | ||
57 | + } | ||
58 | + } | ||
59 | + | ||
60 | + @Override | ||
61 | + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
62 | + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; | ||
63 | + byte[] bytes = toBytes(inbound.payload()); | ||
64 | + Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); | ||
65 | + try { | ||
66 | + return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); | ||
67 | + } catch (Exception e) { | ||
68 | + log.debug("Failed to decode post attributes request", e); | ||
69 | + throw new AdaptorException(e); | ||
70 | + } | ||
71 | + } | ||
72 | + | ||
73 | + @Override | ||
74 | + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { | ||
75 | + byte[] bytes = toBytes(inbound.payload()); | ||
76 | + try { | ||
77 | + return ProtoConverter.convertToClaimDeviceProto(ctx.getDeviceId(), bytes); | ||
78 | + } catch (InvalidProtocolBufferException e) { | ||
79 | + log.debug("Failed to decode claim device request", e); | ||
80 | + throw new AdaptorException(e); | ||
81 | + } | ||
82 | + } | ||
83 | + | ||
84 | + @Override | ||
85 | + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { | ||
86 | + byte[] bytes = toBytes(inbound.payload()); | ||
87 | + String topicName = inbound.variableHeader().topicName(); | ||
88 | + try { | ||
89 | + int requestId = getRequestId(topicName, topicBase); | ||
90 | + return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); | ||
91 | + } catch (InvalidProtocolBufferException e) { | ||
92 | + log.debug("Failed to decode get attributes request", e); | ||
93 | + throw new AdaptorException(e); | ||
94 | + } | ||
95 | + } | ||
96 | + | ||
97 | + @Override | ||
98 | + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { | ||
99 | + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; | ||
100 | + String topicName = mqttMsg.variableHeader().topicName(); | ||
101 | + byte[] bytes = toBytes(mqttMsg.payload()); | ||
102 | + Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); | ||
103 | + try { | ||
104 | + int requestId = getRequestId(topicName, topicBase); | ||
105 | + JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); | ||
106 | + return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); | ||
107 | + } catch (Exception e) { | ||
108 | + log.debug("Failed to decode rpc response", e); | ||
109 | + throw new AdaptorException(e); | ||
110 | + } | ||
111 | + } | ||
112 | + | ||
113 | + @Override | ||
114 | + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { | ||
115 | + byte[] bytes = toBytes(mqttMsg.payload()); | ||
116 | + String topicName = mqttMsg.variableHeader().topicName(); | ||
117 | + try { | ||
118 | + int requestId = getRequestId(topicName, topicBase); | ||
119 | + return ProtoConverter.convertToServerRpcRequest(bytes, requestId); | ||
120 | + } catch (InvalidProtocolBufferException e) { | ||
121 | + log.debug("Failed to decode to server rpc request", e); | ||
122 | + throw new AdaptorException(e); | ||
123 | + } | ||
124 | + } | ||
125 | + | ||
126 | + @Override | ||
127 | + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { | ||
128 | + byte[] bytes = toBytes(mqttMsg.payload()); | ||
129 | + try { | ||
130 | + return ProtoConverter.convertToProvisionRequestMsg(bytes); | ||
131 | + } catch (InvalidProtocolBufferException ex) { | ||
132 | + log.debug("Failed to decode provision request", ex); | ||
133 | + throw new AdaptorException(ex); | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + @Override | ||
138 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { | ||
139 | + if (!StringUtils.isEmpty(responseMsg.getError())) { | ||
140 | + throw new AdaptorException(responseMsg.getError()); | ||
141 | + } else { | ||
142 | + int requestId = responseMsg.getRequestId(); | ||
143 | + if (requestId >= 0) { | ||
144 | + return Optional.of(createMqttPublishMsg(ctx, topicBase + requestId, responseMsg.toByteArray())); | ||
145 | + } | ||
146 | + return Optional.empty(); | ||
147 | + } | ||
148 | + } | ||
149 | + | ||
150 | + @Override | ||
151 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException { | ||
152 | + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; | ||
153 | + DynamicMessage.Builder rpcRequestDynamicMessageBuilder = deviceSessionCtx.getRpcRequestDynamicMessageBuilder(); | ||
154 | + if (rpcRequestDynamicMessageBuilder == null) { | ||
155 | + throw new AdaptorException("Failed to get rpcRequestDynamicMessageBuilder!"); | ||
156 | + } else { | ||
157 | + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder))); | ||
158 | + } | ||
159 | + } | ||
160 | + | ||
161 | + @Override | ||
162 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) { | ||
163 | + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcResponse.getRequestId(), rpcResponse.toByteArray())); | ||
164 | + } | ||
165 | + | ||
166 | + @Override | ||
167 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { | ||
168 | + return Optional.of(createMqttPublishMsg(ctx, topic, notificationMsg.toByteArray())); | ||
169 | + } | ||
170 | + | ||
171 | + @Override | ||
172 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) { | ||
173 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC, provisionResponse.toByteArray())); | ||
174 | + } | ||
175 | + | ||
176 | + @Override | ||
177 | + public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException { | ||
178 | + return Optional.of(createMqttPublishMsg(ctx, String.format(DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT, firmwareType.getKeyPrefix(), requestId, chunk), firmwareChunk)); | ||
179 | + } | ||
180 | + | ||
181 | + @Override | ||
182 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { | ||
183 | + if (!StringUtils.isEmpty(responseMsg.getError())) { | ||
184 | + throw new AdaptorException(responseMsg.getError()); | ||
185 | + } else { | ||
186 | + TransportApiProtos.GatewayAttributeResponseMsg.Builder responseMsgBuilder = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); | ||
187 | + responseMsgBuilder.setDeviceName(deviceName); | ||
188 | + responseMsgBuilder.setResponseMsg(responseMsg); | ||
189 | + byte[] payloadBytes = responseMsgBuilder.build().toByteArray(); | ||
190 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, payloadBytes)); | ||
191 | + } | ||
192 | + } | ||
193 | + | ||
194 | + @Override | ||
195 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { | ||
196 | + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder builder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); | ||
197 | + builder.setDeviceName(deviceName); | ||
198 | + builder.setNotificationMsg(notificationMsg); | ||
199 | + byte[] payloadBytes = builder.build().toByteArray(); | ||
200 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, payloadBytes)); | ||
201 | + } | ||
202 | + | ||
203 | + @Override | ||
204 | + public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { | ||
205 | + TransportApiProtos.GatewayDeviceRpcRequestMsg.Builder builder = TransportApiProtos.GatewayDeviceRpcRequestMsg.newBuilder(); | ||
206 | + builder.setDeviceName(deviceName); | ||
207 | + builder.setRpcRequestMsg(rpcRequest); | ||
208 | + byte[] payloadBytes = builder.build().toByteArray(); | ||
209 | + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_RPC_TOPIC, payloadBytes)); | ||
210 | + } | ||
211 | + | ||
212 | + public static byte[] toBytes(ByteBuf inbound) { | ||
213 | + byte[] bytes = new byte[inbound.readableBytes()]; | ||
214 | + int readerIndex = inbound.readerIndex(); | ||
215 | + inbound.getBytes(readerIndex, bytes); | ||
216 | + return bytes; | ||
217 | + } | ||
218 | + | ||
219 | + private int getRequestId(String topicName, String topic) { | ||
220 | + return Integer.parseInt(topicName.substring(topic.length())); | ||
221 | + } | ||
222 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.adaptors; | ||
17 | + | ||
18 | +import io.netty.buffer.ByteBuf; | ||
19 | +import io.netty.buffer.ByteBufAllocator; | ||
20 | +import io.netty.buffer.UnpooledByteBufAllocator; | ||
21 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | ||
22 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
23 | +import io.netty.handler.codec.mqtt.MqttMessageType; | ||
24 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
25 | +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; | ||
26 | +import org.thingsboard.server.common.data.ota.OtaPackageType; | ||
27 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
28 | +import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; | ||
29 | +import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; | ||
30 | +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; | ||
31 | +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; | ||
32 | +import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; | ||
33 | +import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; | ||
34 | +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; | ||
35 | +import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; | ||
36 | +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; | ||
37 | +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; | ||
38 | +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; | ||
39 | +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; | ||
40 | +import org.thingsboard.server.transport.tcp.session.MqttDeviceAwareSessionContext; | ||
41 | + | ||
42 | +import java.util.Optional; | ||
43 | + | ||
44 | +/** | ||
45 | + * @author Andrew Shvayka | ||
46 | + */ | ||
47 | +public interface TcpTransportAdaptor { | ||
48 | + | ||
49 | + ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); | ||
50 | + | ||
51 | + PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; | ||
52 | + | ||
53 | + PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; | ||
54 | + | ||
55 | + GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException; | ||
56 | + | ||
57 | + ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException; | ||
58 | + | ||
59 | + ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException; | ||
60 | + | ||
61 | + ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; | ||
62 | + | ||
63 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException; | ||
64 | + | ||
65 | + Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, GetAttributeResponseMsg responseMsg) throws AdaptorException; | ||
66 | + | ||
67 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, AttributeUpdateNotificationMsg notificationMsg, String topic) throws AdaptorException; | ||
68 | + | ||
69 | + Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; | ||
70 | + | ||
71 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException; | ||
72 | + | ||
73 | + Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; | ||
74 | + | ||
75 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse, String topicBase) throws AdaptorException; | ||
76 | + | ||
77 | + ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; | ||
78 | + | ||
79 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException; | ||
80 | + | ||
81 | + Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException; | ||
82 | + | ||
83 | + default MqttPublishMessage createMqttPublishMsg(MqttDeviceAwareSessionContext ctx, String topic, byte[] payloadInBytes) { | ||
84 | + MqttFixedHeader mqttFixedHeader = | ||
85 | + new MqttFixedHeader(MqttMessageType.PUBLISH, false, ctx.getQoSForTopic(topic), false, 0); | ||
86 | + MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId()); | ||
87 | + ByteBuf payload = ALLOCATOR.buffer(); | ||
88 | + payload.writeBytes(payloadInBytes); | ||
89 | + return new MqttPublishMessage(mqttFixedHeader, header, payload); | ||
90 | + } | ||
91 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/limits/IpFilter.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.limits; | ||
17 | + | ||
18 | +import io.netty.channel.ChannelHandlerContext; | ||
19 | +import io.netty.handler.ipfilter.AbstractRemoteAddressFilter; | ||
20 | +import lombok.extern.slf4j.Slf4j; | ||
21 | +import org.thingsboard.server.transport.tcp.TcpTransportContext; | ||
22 | +import org.thingsboard.server.transport.tcp.TcpTransportService; | ||
23 | + | ||
24 | +import java.net.InetSocketAddress; | ||
25 | + | ||
26 | +@Slf4j | ||
27 | +public class IpFilter extends AbstractRemoteAddressFilter<InetSocketAddress> { | ||
28 | + | ||
29 | + private TcpTransportContext context; | ||
30 | + | ||
31 | + public IpFilter(TcpTransportContext context) { | ||
32 | + this.context = context; | ||
33 | + } | ||
34 | + | ||
35 | + @Override | ||
36 | + protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) throws Exception { | ||
37 | + log.trace("[{}] Received msg: {}", ctx.channel().id(), remoteAddress); | ||
38 | + if(context.checkAddress(remoteAddress)){ | ||
39 | + log.trace("[{}] Setting address: {}", ctx.channel().id(), remoteAddress); | ||
40 | + ctx.channel().attr(TcpTransportService.ADDRESS).set(remoteAddress); | ||
41 | + return true; | ||
42 | + } else { | ||
43 | + return false; | ||
44 | + } | ||
45 | + } | ||
46 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/limits/ProxyIpFilter.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.limits; | ||
17 | + | ||
18 | +import io.netty.channel.ChannelHandler; | ||
19 | +import io.netty.channel.ChannelHandlerContext; | ||
20 | +import io.netty.channel.ChannelInboundHandlerAdapter; | ||
21 | +import io.netty.handler.codec.haproxy.HAProxyMessage; | ||
22 | +import lombok.extern.slf4j.Slf4j; | ||
23 | +import org.thingsboard.server.transport.tcp.TcpTransportContext; | ||
24 | +import org.thingsboard.server.transport.tcp.TcpTransportService; | ||
25 | + | ||
26 | +import java.net.InetSocketAddress; | ||
27 | + | ||
28 | +@Slf4j | ||
29 | +public class ProxyIpFilter extends ChannelInboundHandlerAdapter { | ||
30 | + | ||
31 | + | ||
32 | + private TcpTransportContext context; | ||
33 | + | ||
34 | + public ProxyIpFilter(TcpTransportContext context) { | ||
35 | + this.context = context; | ||
36 | + } | ||
37 | + | ||
38 | + @Override | ||
39 | + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { | ||
40 | + log.trace("[{}] Received msg: {}", ctx.channel().id(), msg); | ||
41 | + if (msg instanceof HAProxyMessage) { | ||
42 | + HAProxyMessage proxyMsg = (HAProxyMessage) msg; | ||
43 | + if (proxyMsg.sourceAddress() != null && proxyMsg.sourcePort() > 0) { | ||
44 | + InetSocketAddress address = new InetSocketAddress(proxyMsg.sourceAddress(), proxyMsg.sourcePort()); | ||
45 | + if (!context.checkAddress(address)) { | ||
46 | + closeChannel(ctx); | ||
47 | + } else { | ||
48 | + log.trace("[{}] Setting address: {}", ctx.channel().id(), address); | ||
49 | + ctx.channel().attr(TcpTransportService.ADDRESS).set(address); | ||
50 | + // We no longer need this channel in the pipeline. Similar to HAProxyMessageDecoder | ||
51 | + ctx.pipeline().remove(this); | ||
52 | + } | ||
53 | + } else { | ||
54 | + log.trace("Received local health-check connection message: {}", proxyMsg); | ||
55 | + closeChannel(ctx); | ||
56 | + } | ||
57 | + } | ||
58 | + } | ||
59 | + | ||
60 | + private void closeChannel(ChannelHandlerContext ctx) { | ||
61 | + while (ctx.pipeline().last() != this) { | ||
62 | + ChannelHandler handler = ctx.pipeline().removeLast(); | ||
63 | + if (handler instanceof ChannelInboundHandlerAdapter) { | ||
64 | + try { | ||
65 | + ((ChannelInboundHandlerAdapter) handler).channelUnregistered(ctx); | ||
66 | + } catch (Exception e) { | ||
67 | + log.error("Failed to unregister channel: [{}]", ctx, e); | ||
68 | + } | ||
69 | + } | ||
70 | + | ||
71 | + } | ||
72 | + ctx.pipeline().remove(this); | ||
73 | + ctx.close(); | ||
74 | + } | ||
75 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.session; | ||
17 | + | ||
18 | +import com.google.protobuf.Descriptors; | ||
19 | +import com.google.protobuf.DynamicMessage; | ||
20 | +import io.netty.channel.ChannelHandlerContext; | ||
21 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
22 | +import io.netty.util.ReferenceCountUtil; | ||
23 | +import lombok.Getter; | ||
24 | +import lombok.Setter; | ||
25 | +import lombok.extern.slf4j.Slf4j; | ||
26 | +import org.thingsboard.server.common.data.DeviceProfile; | ||
27 | +import org.thingsboard.server.common.data.DeviceTransportType; | ||
28 | +import org.thingsboard.server.common.data.TransportPayloadType; | ||
29 | +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; | ||
30 | +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; | ||
31 | +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; | ||
32 | +import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; | ||
33 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
34 | +import org.thingsboard.server.transport.tcp.TcpTransportContext; | ||
35 | +import org.thingsboard.server.transport.tcp.TopicType; | ||
36 | +import org.thingsboard.server.transport.tcp.adaptors.BackwardCompatibilityAdaptor; | ||
37 | +import org.thingsboard.server.transport.tcp.adaptors.TcpTransportAdaptor; | ||
38 | +import org.thingsboard.server.transport.tcp.util.TcpTopicFilter; | ||
39 | +import org.thingsboard.server.transport.tcp.util.MqttTopicFilterFactory; | ||
40 | + | ||
41 | +import java.util.Collection; | ||
42 | +import java.util.Collections; | ||
43 | +import java.util.UUID; | ||
44 | +import java.util.concurrent.ConcurrentLinkedQueue; | ||
45 | +import java.util.concurrent.ConcurrentMap; | ||
46 | +import java.util.concurrent.atomic.AtomicInteger; | ||
47 | +import java.util.concurrent.locks.Lock; | ||
48 | +import java.util.concurrent.locks.ReentrantLock; | ||
49 | +import java.util.function.Consumer; | ||
50 | + | ||
51 | +/** | ||
52 | + * @author Andrew Shvayka | ||
53 | + */ | ||
54 | +@Slf4j | ||
55 | +public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { | ||
56 | + | ||
57 | + @Getter | ||
58 | + @Setter | ||
59 | + private ChannelHandlerContext channel; | ||
60 | + | ||
61 | + @Getter | ||
62 | + private final TcpTransportContext context; | ||
63 | + | ||
64 | + private final AtomicInteger msgIdSeq = new AtomicInteger(0); | ||
65 | + | ||
66 | + private final ConcurrentLinkedQueue<MqttMessage> msgQueue = new ConcurrentLinkedQueue<>(); | ||
67 | + | ||
68 | + @Getter | ||
69 | + private final Lock msgQueueProcessorLock = new ReentrantLock(); | ||
70 | + | ||
71 | + private final AtomicInteger msgQueueSize = new AtomicInteger(0); | ||
72 | + | ||
73 | + @Getter | ||
74 | + @Setter | ||
75 | + private boolean provisionOnly = false; | ||
76 | + | ||
77 | + private volatile TcpTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); | ||
78 | + private volatile TcpTopicFilter attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); | ||
79 | + private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; | ||
80 | + private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor; | ||
81 | + private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; | ||
82 | + private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; | ||
83 | + private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; | ||
84 | + private volatile TcpTransportAdaptor adaptor; | ||
85 | + private volatile boolean jsonPayloadFormatCompatibilityEnabled; | ||
86 | + private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics; | ||
87 | + | ||
88 | + @Getter | ||
89 | + @Setter | ||
90 | + private TransportPayloadType provisionPayloadType = payloadType; | ||
91 | + | ||
92 | + public DeviceSessionCtx(UUID sessionId, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap, TcpTransportContext context) { | ||
93 | + super(sessionId, mqttQoSMap); | ||
94 | + this.context = context; | ||
95 | + this.adaptor = context.getJsonMqttAdaptor(); | ||
96 | + } | ||
97 | + | ||
98 | + public int nextMsgId() { | ||
99 | + return msgIdSeq.incrementAndGet(); | ||
100 | + } | ||
101 | + | ||
102 | + public boolean isDeviceTelemetryTopic(String topicName) { | ||
103 | + return telemetryTopicFilter.filter(topicName); | ||
104 | + } | ||
105 | + | ||
106 | + public boolean isDeviceAttributesTopic(String topicName) { | ||
107 | + return attributesTopicFilter.filter(topicName); | ||
108 | + } | ||
109 | + | ||
110 | + public TcpTransportAdaptor getPayloadAdaptor() { | ||
111 | + return adaptor; | ||
112 | + } | ||
113 | + | ||
114 | + public boolean isJsonPayloadType() { | ||
115 | + return payloadType.equals(TransportPayloadType.JSON); | ||
116 | + } | ||
117 | + | ||
118 | + public Descriptors.Descriptor getTelemetryDynamicMsgDescriptor() { | ||
119 | + return telemetryDynamicMessageDescriptor; | ||
120 | + } | ||
121 | + | ||
122 | + public Descriptors.Descriptor getAttributesDynamicMessageDescriptor() { | ||
123 | + return attributesDynamicMessageDescriptor; | ||
124 | + } | ||
125 | + | ||
126 | + public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor() { | ||
127 | + return rpcResponseDynamicMessageDescriptor; | ||
128 | + } | ||
129 | + | ||
130 | + public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder() { | ||
131 | + return rpcRequestDynamicMessageBuilder; | ||
132 | + } | ||
133 | + | ||
134 | + @Override | ||
135 | + public void setDeviceProfile(DeviceProfile deviceProfile) { | ||
136 | + super.setDeviceProfile(deviceProfile); | ||
137 | + updateDeviceSessionConfiguration(deviceProfile); | ||
138 | + } | ||
139 | + | ||
140 | + @Override | ||
141 | + public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { | ||
142 | + super.onDeviceProfileUpdate(sessionInfo, deviceProfile); | ||
143 | + updateDeviceSessionConfiguration(deviceProfile); | ||
144 | + } | ||
145 | + | ||
146 | + private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) { | ||
147 | + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); | ||
148 | + if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && | ||
149 | + transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { | ||
150 | + MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration; | ||
151 | + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration(); | ||
152 | + payloadType = transportPayloadTypeConfiguration.getTransportPayloadType(); | ||
153 | + telemetryTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceTelemetryTopic()); | ||
154 | + attributesTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesTopic()); | ||
155 | + if (TransportPayloadType.PROTOBUF.equals(payloadType)) { | ||
156 | + ProtoTransportPayloadConfiguration protoTransportPayloadConfig = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; | ||
157 | + updateDynamicMessageDescriptors(protoTransportPayloadConfig); | ||
158 | + jsonPayloadFormatCompatibilityEnabled = protoTransportPayloadConfig.isEnableCompatibilityWithJsonPayloadFormat(); | ||
159 | + useJsonPayloadFormatForDefaultDownlinkTopics = jsonPayloadFormatCompatibilityEnabled && protoTransportPayloadConfig.isUseJsonPayloadFormatForDefaultDownlinkTopics(); | ||
160 | + } | ||
161 | + } else { | ||
162 | + telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); | ||
163 | + attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); | ||
164 | + payloadType = TransportPayloadType.JSON; | ||
165 | + } | ||
166 | + updateAdaptor(); | ||
167 | + } | ||
168 | + | ||
169 | + private void updateDynamicMessageDescriptors(ProtoTransportPayloadConfiguration protoTransportPayloadConfig) { | ||
170 | + telemetryDynamicMessageDescriptor = protoTransportPayloadConfig.getTelemetryDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceTelemetryProtoSchema()); | ||
171 | + attributesDynamicMessageDescriptor = protoTransportPayloadConfig.getAttributesDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceAttributesProtoSchema()); | ||
172 | + rpcResponseDynamicMessageDescriptor = protoTransportPayloadConfig.getRpcResponseDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceRpcResponseProtoSchema()); | ||
173 | + rpcRequestDynamicMessageBuilder = protoTransportPayloadConfig.getRpcRequestDynamicMessageBuilder(protoTransportPayloadConfig.getDeviceRpcRequestProtoSchema()); | ||
174 | + } | ||
175 | + | ||
176 | + public TcpTransportAdaptor getAdaptor(TopicType topicType) { | ||
177 | + switch (topicType) { | ||
178 | + case V2: | ||
179 | + return getDefaultAdaptor(); | ||
180 | + case V2_JSON: | ||
181 | + return context.getJsonMqttAdaptor(); | ||
182 | + case V2_PROTO: | ||
183 | + return context.getProtoMqttAdaptor(); | ||
184 | + default: | ||
185 | + return useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor(); | ||
186 | + } | ||
187 | + } | ||
188 | + | ||
189 | + private TcpTransportAdaptor getDefaultAdaptor() { | ||
190 | + return isJsonPayloadType() ? context.getJsonMqttAdaptor() : context.getProtoMqttAdaptor(); | ||
191 | + } | ||
192 | + | ||
193 | + private void updateAdaptor() { | ||
194 | + if (isJsonPayloadType()) { | ||
195 | + adaptor = context.getJsonMqttAdaptor(); | ||
196 | + jsonPayloadFormatCompatibilityEnabled = false; | ||
197 | + useJsonPayloadFormatForDefaultDownlinkTopics = false; | ||
198 | + } else { | ||
199 | + if (jsonPayloadFormatCompatibilityEnabled) { | ||
200 | + adaptor = new BackwardCompatibilityAdaptor(context.getProtoMqttAdaptor(), context.getJsonMqttAdaptor()); | ||
201 | + } else { | ||
202 | + adaptor = context.getProtoMqttAdaptor(); | ||
203 | + } | ||
204 | + } | ||
205 | + } | ||
206 | + | ||
207 | + public void addToQueue(MqttMessage msg) { | ||
208 | + msgQueueSize.incrementAndGet(); | ||
209 | + ReferenceCountUtil.retain(msg); | ||
210 | + msgQueue.add(msg); | ||
211 | + } | ||
212 | + | ||
213 | + public void tryProcessQueuedMsgs(Consumer<MqttMessage> msgProcessor) { | ||
214 | + while (!msgQueue.isEmpty()) { | ||
215 | + if (msgQueueProcessorLock.tryLock()) { | ||
216 | + try { | ||
217 | + MqttMessage msg; | ||
218 | + while ((msg = msgQueue.poll()) != null) { | ||
219 | + try { | ||
220 | + msgQueueSize.decrementAndGet(); | ||
221 | + msgProcessor.accept(msg); | ||
222 | + } finally { | ||
223 | + ReferenceCountUtil.safeRelease(msg); | ||
224 | + } | ||
225 | + } | ||
226 | + } finally { | ||
227 | + msgQueueProcessorLock.unlock(); | ||
228 | + } | ||
229 | + } else { | ||
230 | + return; | ||
231 | + } | ||
232 | + } | ||
233 | + } | ||
234 | + | ||
235 | + public int getMsgQueueSize() { | ||
236 | + return msgQueueSize.get(); | ||
237 | + } | ||
238 | + | ||
239 | + public void release() { | ||
240 | + if (!msgQueue.isEmpty()) { | ||
241 | + log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", getDeviceId(), msgQueue.size()); | ||
242 | + msgQueue.forEach(ReferenceCountUtil::safeRelease); | ||
243 | + msgQueue.clear(); | ||
244 | + } | ||
245 | + } | ||
246 | + | ||
247 | + public Collection<MqttMessage> getMsgQueueSnapshot(){ | ||
248 | + return Collections.unmodifiableCollection(msgQueue); | ||
249 | + } | ||
250 | + | ||
251 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.session; | ||
17 | + | ||
18 | +import io.netty.channel.ChannelFuture; | ||
19 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
20 | +import lombok.extern.slf4j.Slf4j; | ||
21 | +import org.thingsboard.server.common.data.DeviceProfile; | ||
22 | +import org.thingsboard.server.common.data.id.DeviceId; | ||
23 | +import org.thingsboard.server.common.data.rpc.RpcStatus; | ||
24 | +import org.thingsboard.server.common.transport.SessionMsgListener; | ||
25 | +import org.thingsboard.server.common.transport.TransportService; | ||
26 | +import org.thingsboard.server.common.transport.TransportServiceCallback; | ||
27 | +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; | ||
28 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
29 | +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; | ||
30 | + | ||
31 | +import java.util.UUID; | ||
32 | +import java.util.concurrent.ConcurrentMap; | ||
33 | + | ||
34 | +/** | ||
35 | + * Created by ashvayka on 19.01.17. | ||
36 | + */ | ||
37 | +@Slf4j | ||
38 | +public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext implements SessionMsgListener { | ||
39 | + | ||
40 | + private final GatewaySessionHandler parent; | ||
41 | + private final TransportService transportService; | ||
42 | + | ||
43 | + public GatewayDeviceSessionCtx(GatewaySessionHandler parent, TransportDeviceInfo deviceInfo, | ||
44 | + DeviceProfile deviceProfile, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap, | ||
45 | + TransportService transportService) { | ||
46 | + super(UUID.randomUUID(), mqttQoSMap); | ||
47 | + this.parent = parent; | ||
48 | + setSessionInfo(SessionInfoProto.newBuilder() | ||
49 | + .setNodeId(parent.getNodeId()) | ||
50 | + .setSessionIdMSB(sessionId.getMostSignificantBits()) | ||
51 | + .setSessionIdLSB(sessionId.getLeastSignificantBits()) | ||
52 | + .setDeviceIdMSB(deviceInfo.getDeviceId().getId().getMostSignificantBits()) | ||
53 | + .setDeviceIdLSB(deviceInfo.getDeviceId().getId().getLeastSignificantBits()) | ||
54 | + .setTenantIdMSB(deviceInfo.getTenantId().getId().getMostSignificantBits()) | ||
55 | + .setTenantIdLSB(deviceInfo.getTenantId().getId().getLeastSignificantBits()) | ||
56 | + .setCustomerIdMSB(deviceInfo.getCustomerId().getId().getMostSignificantBits()) | ||
57 | + .setCustomerIdLSB(deviceInfo.getCustomerId().getId().getLeastSignificantBits()) | ||
58 | + .setDeviceName(deviceInfo.getDeviceName()) | ||
59 | + .setDeviceType(deviceInfo.getDeviceType()) | ||
60 | + .setGwSessionIdMSB(parent.getSessionId().getMostSignificantBits()) | ||
61 | + .setGwSessionIdLSB(parent.getSessionId().getLeastSignificantBits()) | ||
62 | + .setDeviceProfileIdMSB(deviceInfo.getDeviceProfileId().getId().getMostSignificantBits()) | ||
63 | + .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) | ||
64 | + .build()); | ||
65 | + setDeviceInfo(deviceInfo); | ||
66 | + setConnected(true); | ||
67 | + setDeviceProfile(deviceProfile); | ||
68 | + this.transportService = transportService; | ||
69 | + } | ||
70 | + | ||
71 | + @Override | ||
72 | + public UUID getSessionId() { | ||
73 | + return sessionId; | ||
74 | + } | ||
75 | + | ||
76 | + @Override | ||
77 | + public int nextMsgId() { | ||
78 | + return parent.nextMsgId(); | ||
79 | + } | ||
80 | + | ||
81 | + @Override | ||
82 | + public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { | ||
83 | + try { | ||
84 | + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), response).ifPresent(parent::writeAndFlush); | ||
85 | + } catch (Exception e) { | ||
86 | + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); | ||
87 | + } | ||
88 | + } | ||
89 | + | ||
90 | + @Override | ||
91 | + public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { | ||
92 | + log.trace("[{}] Received attributes update notification to device", sessionId); | ||
93 | + try { | ||
94 | + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), notification).ifPresent(parent::writeAndFlush); | ||
95 | + } catch (Exception e) { | ||
96 | + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + @Override | ||
101 | + public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg request) { | ||
102 | + log.trace("[{}] Received RPC command to device", sessionId); | ||
103 | + try { | ||
104 | + parent.getPayloadAdaptor().convertToGatewayPublish(this, getDeviceInfo().getDeviceName(), request).ifPresent( | ||
105 | + payload -> { | ||
106 | + ChannelFuture channelFuture = parent.writeAndFlush(payload); | ||
107 | + if (request.getPersisted()) { | ||
108 | + channelFuture.addListener(result -> { | ||
109 | + if (result.cause() == null) { | ||
110 | + if (!isAckExpected(payload)) { | ||
111 | + transportService.process(getSessionInfo(), request, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); | ||
112 | + } else if (request.getPersisted()) { | ||
113 | + transportService.process(getSessionInfo(), request, RpcStatus.SENT, TransportServiceCallback.EMPTY); | ||
114 | + | ||
115 | + } | ||
116 | + } | ||
117 | + }); | ||
118 | + } | ||
119 | + } | ||
120 | + ); | ||
121 | + } catch (Exception e) { | ||
122 | + transportService.process(getSessionInfo(), | ||
123 | + TransportProtos.ToDeviceRpcResponseMsg.newBuilder() | ||
124 | + .setRequestId(request.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY); | ||
125 | + log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); | ||
126 | + } | ||
127 | + } | ||
128 | + | ||
129 | + @Override | ||
130 | + public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { | ||
131 | + log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); | ||
132 | + parent.deregisterSession(getDeviceInfo().getDeviceName()); | ||
133 | + } | ||
134 | + | ||
135 | + @Override | ||
136 | + public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { | ||
137 | + // This feature is not supported in the TB IoT Gateway yet. | ||
138 | + } | ||
139 | + | ||
140 | + @Override | ||
141 | + public void onDeviceDeleted(DeviceId deviceId) { | ||
142 | + parent.onDeviceDeleted(this.getSessionInfo().getDeviceName()); | ||
143 | + } | ||
144 | + | ||
145 | + private boolean isAckExpected(MqttMessage message) { | ||
146 | + return message.fixedHeader().qosLevel().value() > 0; | ||
147 | + } | ||
148 | + | ||
149 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.session; | ||
17 | + | ||
18 | + | ||
19 | +import com.google.common.util.concurrent.FutureCallback; | ||
20 | +import com.google.common.util.concurrent.Futures; | ||
21 | +import com.google.common.util.concurrent.ListenableFuture; | ||
22 | +import com.google.common.util.concurrent.SettableFuture; | ||
23 | +import com.google.gson.JsonArray; | ||
24 | +import com.google.gson.JsonElement; | ||
25 | +import com.google.gson.JsonNull; | ||
26 | +import com.google.gson.JsonObject; | ||
27 | +import com.google.gson.JsonSyntaxException; | ||
28 | +import com.google.protobuf.InvalidProtocolBufferException; | ||
29 | +import com.google.protobuf.ProtocolStringList; | ||
30 | +import io.netty.buffer.ByteBuf; | ||
31 | +import io.netty.channel.ChannelFuture; | ||
32 | +import io.netty.channel.ChannelHandlerContext; | ||
33 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
34 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
35 | +import lombok.extern.slf4j.Slf4j; | ||
36 | +import org.springframework.util.CollectionUtils; | ||
37 | +import org.springframework.util.ConcurrentReferenceHashMap; | ||
38 | +import org.springframework.util.StringUtils; | ||
39 | +import org.thingsboard.server.common.data.id.DeviceId; | ||
40 | +import org.thingsboard.server.common.transport.TransportService; | ||
41 | +import org.thingsboard.server.common.transport.TransportServiceCallback; | ||
42 | +import org.thingsboard.server.common.transport.adaptor.AdaptorException; | ||
43 | +import org.thingsboard.server.common.transport.adaptor.JsonConverter; | ||
44 | +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; | ||
45 | +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; | ||
46 | +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; | ||
47 | +import org.thingsboard.server.gen.transport.TransportApiProtos; | ||
48 | +import org.thingsboard.server.gen.transport.TransportProtos; | ||
49 | +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; | ||
50 | +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; | ||
51 | +import org.thingsboard.server.transport.tcp.TcpTransportContext; | ||
52 | +import org.thingsboard.server.transport.tcp.TcpTransportHandler; | ||
53 | +import org.thingsboard.server.transport.tcp.adaptors.JsonTcpAdaptor; | ||
54 | +import org.thingsboard.server.transport.tcp.adaptors.TcpTransportAdaptor; | ||
55 | +import org.thingsboard.server.transport.tcp.adaptors.ProtoTcpAdaptor; | ||
56 | + | ||
57 | +import javax.annotation.Nullable; | ||
58 | +import java.util.Collections; | ||
59 | +import java.util.HashSet; | ||
60 | +import java.util.List; | ||
61 | +import java.util.Map; | ||
62 | +import java.util.Set; | ||
63 | +import java.util.UUID; | ||
64 | +import java.util.concurrent.ConcurrentHashMap; | ||
65 | +import java.util.concurrent.ConcurrentMap; | ||
66 | +import java.util.concurrent.locks.Lock; | ||
67 | +import java.util.concurrent.locks.ReentrantLock; | ||
68 | + | ||
69 | +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; | ||
70 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; | ||
71 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; | ||
72 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; | ||
73 | +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; | ||
74 | + | ||
75 | +/** | ||
76 | + * Created by ashvayka on 19.01.17. | ||
77 | + */ | ||
78 | +@Slf4j | ||
79 | +public class GatewaySessionHandler { | ||
80 | + | ||
81 | + private static final String DEFAULT_DEVICE_TYPE = "default"; | ||
82 | + private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; | ||
83 | + private static final String DEVICE_PROPERTY = "device"; | ||
84 | + | ||
85 | + private final TcpTransportContext context; | ||
86 | + private final TransportService transportService; | ||
87 | + private final TransportDeviceInfo gateway; | ||
88 | + private final UUID sessionId; | ||
89 | + private final ConcurrentMap<String, Lock> deviceCreationLockMap; | ||
90 | + private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; | ||
91 | + private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures; | ||
92 | + private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; | ||
93 | + private final ChannelHandlerContext channel; | ||
94 | + private final DeviceSessionCtx deviceSessionCtx; | ||
95 | + | ||
96 | + public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { | ||
97 | + this.context = deviceSessionCtx.getContext(); | ||
98 | + this.transportService = context.getTransportService(); | ||
99 | + this.deviceSessionCtx = deviceSessionCtx; | ||
100 | + this.gateway = deviceSessionCtx.getDeviceInfo(); | ||
101 | + this.sessionId = sessionId; | ||
102 | + this.devices = new ConcurrentHashMap<>(); | ||
103 | + this.deviceFutures = new ConcurrentHashMap<>(); | ||
104 | + this.deviceCreationLockMap = createWeakMap(); | ||
105 | + this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); | ||
106 | + this.channel = deviceSessionCtx.getChannel(); | ||
107 | + } | ||
108 | + | ||
109 | + ConcurrentReferenceHashMap<String, Lock> createWeakMap() { | ||
110 | + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); | ||
111 | + } | ||
112 | + | ||
113 | + public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
114 | + if (isJsonPayloadType()) { | ||
115 | + onDeviceConnectJson(mqttMsg); | ||
116 | + } else { | ||
117 | + onDeviceConnectProto(mqttMsg); | ||
118 | + } | ||
119 | + } | ||
120 | + | ||
121 | + public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
122 | + if (isJsonPayloadType()) { | ||
123 | + onDeviceDisconnectJson(mqttMsg); | ||
124 | + } else { | ||
125 | + onDeviceDisconnectProto(mqttMsg); | ||
126 | + } | ||
127 | + } | ||
128 | + | ||
129 | + public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
130 | + int msgId = getMsgId(mqttMsg); | ||
131 | + ByteBuf payload = mqttMsg.payload(); | ||
132 | + if (isJsonPayloadType()) { | ||
133 | + onDeviceTelemetryJson(msgId, payload); | ||
134 | + } else { | ||
135 | + onDeviceTelemetryProto(msgId, payload); | ||
136 | + } | ||
137 | + } | ||
138 | + | ||
139 | + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
140 | + int msgId = getMsgId(mqttMsg); | ||
141 | + ByteBuf payload = mqttMsg.payload(); | ||
142 | + if (isJsonPayloadType()) { | ||
143 | + onDeviceClaimJson(msgId, payload); | ||
144 | + } else { | ||
145 | + onDeviceClaimProto(msgId, payload); | ||
146 | + } | ||
147 | + } | ||
148 | + | ||
149 | + public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
150 | + int msgId = getMsgId(mqttMsg); | ||
151 | + ByteBuf payload = mqttMsg.payload(); | ||
152 | + if (isJsonPayloadType()) { | ||
153 | + onDeviceAttributesJson(msgId, payload); | ||
154 | + } else { | ||
155 | + onDeviceAttributesProto(msgId, payload); | ||
156 | + } | ||
157 | + } | ||
158 | + | ||
159 | + public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
160 | + if (isJsonPayloadType()) { | ||
161 | + onDeviceAttributesRequestJson(mqttMsg); | ||
162 | + } else { | ||
163 | + onDeviceAttributesRequestProto(mqttMsg); | ||
164 | + } | ||
165 | + } | ||
166 | + | ||
167 | + public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
168 | + int msgId = getMsgId(mqttMsg); | ||
169 | + ByteBuf payload = mqttMsg.payload(); | ||
170 | + if (isJsonPayloadType()) { | ||
171 | + onDeviceRpcResponseJson(msgId, payload); | ||
172 | + } else { | ||
173 | + onDeviceRpcResponseProto(msgId, payload); | ||
174 | + } | ||
175 | + } | ||
176 | + | ||
177 | + public void onGatewayDisconnect() { | ||
178 | + devices.forEach(this::deregisterSession); | ||
179 | + } | ||
180 | + | ||
181 | + public void onDeviceDeleted(String deviceName) { | ||
182 | + deregisterSession(deviceName); | ||
183 | + } | ||
184 | + | ||
185 | + public String getNodeId() { | ||
186 | + return context.getNodeId(); | ||
187 | + } | ||
188 | + | ||
189 | + public UUID getSessionId() { | ||
190 | + return sessionId; | ||
191 | + } | ||
192 | + | ||
193 | + public TcpTransportAdaptor getPayloadAdaptor() { | ||
194 | + return deviceSessionCtx.getPayloadAdaptor(); | ||
195 | + } | ||
196 | + | ||
197 | + void deregisterSession(String deviceName) { | ||
198 | + GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); | ||
199 | + if (deviceSessionCtx != null) { | ||
200 | + deregisterSession(deviceName, deviceSessionCtx); | ||
201 | + } else { | ||
202 | + log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); | ||
203 | + } | ||
204 | + } | ||
205 | + | ||
206 | + ChannelFuture writeAndFlush(MqttMessage mqttMessage) { | ||
207 | + return channel.writeAndFlush(mqttMessage); | ||
208 | + } | ||
209 | + | ||
210 | + int nextMsgId() { | ||
211 | + return deviceSessionCtx.nextMsgId(); | ||
212 | + } | ||
213 | + | ||
214 | + private boolean isJsonPayloadType() { | ||
215 | + return deviceSessionCtx.isJsonPayloadType(); | ||
216 | + } | ||
217 | + | ||
218 | + private void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { | ||
219 | + log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); | ||
220 | + Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback<GatewayDeviceSessionCtx>() { | ||
221 | + @Override | ||
222 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx result) { | ||
223 | + ack(msg); | ||
224 | + log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); | ||
225 | + } | ||
226 | + | ||
227 | + @Override | ||
228 | + public void onFailure(Throwable t) { | ||
229 | + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); | ||
230 | + | ||
231 | + } | ||
232 | + }, context.getExecutor()); | ||
233 | + } | ||
234 | + | ||
235 | + private ListenableFuture<GatewayDeviceSessionCtx> onDeviceConnect(String deviceName, String deviceType) { | ||
236 | + GatewayDeviceSessionCtx result = devices.get(deviceName); | ||
237 | + if (result == null) { | ||
238 | + Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); | ||
239 | + deviceCreationLock.lock(); | ||
240 | + try { | ||
241 | + result = devices.get(deviceName); | ||
242 | + if (result == null) { | ||
243 | + return getDeviceCreationFuture(deviceName, deviceType); | ||
244 | + } else { | ||
245 | + return Futures.immediateFuture(result); | ||
246 | + } | ||
247 | + } finally { | ||
248 | + deviceCreationLock.unlock(); | ||
249 | + } | ||
250 | + } else { | ||
251 | + return Futures.immediateFuture(result); | ||
252 | + } | ||
253 | + } | ||
254 | + | ||
255 | + private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { | ||
256 | + final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); | ||
257 | + ListenableFuture<GatewayDeviceSessionCtx> future = deviceFutures.putIfAbsent(deviceName, futureToSet); | ||
258 | + if (future != null) { | ||
259 | + return future; | ||
260 | + } | ||
261 | + try { | ||
262 | + transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() | ||
263 | + .setDeviceName(deviceName) | ||
264 | + .setDeviceType(deviceType) | ||
265 | + .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) | ||
266 | + .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()).build(), | ||
267 | + new TransportServiceCallback<GetOrCreateDeviceFromGatewayResponse>() { | ||
268 | + @Override | ||
269 | + public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { | ||
270 | + GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); | ||
271 | + if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { | ||
272 | + log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); | ||
273 | + SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); | ||
274 | + transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); | ||
275 | + transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() | ||
276 | + .setSessionInfo(deviceSessionInfo) | ||
277 | + .setSessionEvent(SESSION_EVENT_MSG_OPEN) | ||
278 | + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) | ||
279 | + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) | ||
280 | + .build(), null); | ||
281 | + } | ||
282 | + futureToSet.set(devices.get(deviceName)); | ||
283 | + deviceFutures.remove(deviceName); | ||
284 | + } | ||
285 | + | ||
286 | + @Override | ||
287 | + public void onError(Throwable e) { | ||
288 | + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); | ||
289 | + futureToSet.setException(e); | ||
290 | + deviceFutures.remove(deviceName); | ||
291 | + } | ||
292 | + }); | ||
293 | + return futureToSet; | ||
294 | + } catch (Throwable e) { | ||
295 | + deviceFutures.remove(deviceName); | ||
296 | + throw e; | ||
297 | + } | ||
298 | + } | ||
299 | + | ||
300 | + private int getMsgId(MqttPublishMessage mqttMsg) { | ||
301 | + return mqttMsg.variableHeader().packetId(); | ||
302 | + } | ||
303 | + | ||
304 | + private void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
305 | + JsonElement json = getJson(mqttMsg); | ||
306 | + String deviceName = checkDeviceName(getDeviceName(json)); | ||
307 | + String deviceType = getDeviceType(json); | ||
308 | + processOnConnect(mqttMsg, deviceName, deviceType); | ||
309 | + } | ||
310 | + | ||
311 | + private void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
312 | + try { | ||
313 | + TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); | ||
314 | + String deviceName = checkDeviceName(connectProto.getDeviceName()); | ||
315 | + String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); | ||
316 | + processOnConnect(mqttMsg, deviceName, deviceType); | ||
317 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
318 | + throw new AdaptorException(e); | ||
319 | + } | ||
320 | + } | ||
321 | + | ||
322 | + private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { | ||
323 | + String deviceName = checkDeviceName(getDeviceName(getJson(msg))); | ||
324 | + processOnDisconnect(msg, deviceName); | ||
325 | + } | ||
326 | + | ||
327 | + private void onDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
328 | + try { | ||
329 | + TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); | ||
330 | + String deviceName = checkDeviceName(connectProto.getDeviceName()); | ||
331 | + processOnDisconnect(mqttMsg, deviceName); | ||
332 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
333 | + throw new AdaptorException(e); | ||
334 | + } | ||
335 | + } | ||
336 | + | ||
337 | + private void processOnDisconnect(MqttPublishMessage msg, String deviceName) { | ||
338 | + deregisterSession(deviceName); | ||
339 | + ack(msg); | ||
340 | + } | ||
341 | + | ||
342 | + private void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { | ||
343 | + JsonElement json = JsonTcpAdaptor.validateJsonPayload(sessionId, payload); | ||
344 | + if (json.isJsonObject()) { | ||
345 | + JsonObject jsonObj = json.getAsJsonObject(); | ||
346 | + for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { | ||
347 | + String deviceName = deviceEntry.getKey(); | ||
348 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
349 | + new FutureCallback<>() { | ||
350 | + @Override | ||
351 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
352 | + if (!deviceEntry.getValue().isJsonArray()) { | ||
353 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
354 | + } | ||
355 | + try { | ||
356 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); | ||
357 | + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); | ||
358 | + } catch (Throwable e) { | ||
359 | + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); | ||
360 | + channel.close(); | ||
361 | + } | ||
362 | + } | ||
363 | + | ||
364 | + @Override | ||
365 | + public void onFailure(Throwable t) { | ||
366 | + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); | ||
367 | + } | ||
368 | + }, context.getExecutor()); | ||
369 | + } | ||
370 | + } else { | ||
371 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
372 | + } | ||
373 | + } | ||
374 | + | ||
375 | + private void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { | ||
376 | + try { | ||
377 | + TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); | ||
378 | + List<TransportApiProtos.TelemetryMsg> deviceMsgList = telemetryMsgProto.getMsgList(); | ||
379 | + if (!CollectionUtils.isEmpty(deviceMsgList)) { | ||
380 | + deviceMsgList.forEach(telemetryMsg -> { | ||
381 | + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); | ||
382 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
383 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
384 | + @Override | ||
385 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
386 | + TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); | ||
387 | + try { | ||
388 | + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); | ||
389 | + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); | ||
390 | + } catch (Throwable e) { | ||
391 | + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); | ||
392 | + channel.close(); | ||
393 | + } | ||
394 | + } | ||
395 | + | ||
396 | + @Override | ||
397 | + public void onFailure(Throwable t) { | ||
398 | + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); | ||
399 | + } | ||
400 | + }, context.getExecutor()); | ||
401 | + }); | ||
402 | + } else { | ||
403 | + log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); | ||
404 | + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); | ||
405 | + } | ||
406 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
407 | + throw new AdaptorException(e); | ||
408 | + } | ||
409 | + } | ||
410 | + | ||
411 | + private void processPostTelemetryMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { | ||
412 | + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); | ||
413 | + } | ||
414 | + | ||
415 | + private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { | ||
416 | + JsonElement json = JsonTcpAdaptor.validateJsonPayload(sessionId, payload); | ||
417 | + if (json.isJsonObject()) { | ||
418 | + JsonObject jsonObj = json.getAsJsonObject(); | ||
419 | + for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { | ||
420 | + String deviceName = deviceEntry.getKey(); | ||
421 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
422 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
423 | + @Override | ||
424 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
425 | + if (!deviceEntry.getValue().isJsonObject()) { | ||
426 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
427 | + } | ||
428 | + try { | ||
429 | + DeviceId deviceId = deviceCtx.getDeviceId(); | ||
430 | + TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); | ||
431 | + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); | ||
432 | + } catch (Throwable e) { | ||
433 | + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); | ||
434 | + } | ||
435 | + } | ||
436 | + | ||
437 | + @Override | ||
438 | + public void onFailure(Throwable t) { | ||
439 | + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); | ||
440 | + } | ||
441 | + }, context.getExecutor()); | ||
442 | + } | ||
443 | + } else { | ||
444 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
445 | + } | ||
446 | + } | ||
447 | + | ||
448 | + private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { | ||
449 | + try { | ||
450 | + TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); | ||
451 | + List<TransportApiProtos.ClaimDeviceMsg> claimMsgList = claimMsgProto.getMsgList(); | ||
452 | + if (!CollectionUtils.isEmpty(claimMsgList)) { | ||
453 | + claimMsgList.forEach(claimDeviceMsg -> { | ||
454 | + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); | ||
455 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
456 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
457 | + @Override | ||
458 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
459 | + TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); | ||
460 | + if (claimRequest == null) { | ||
461 | + throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); | ||
462 | + } | ||
463 | + try { | ||
464 | + DeviceId deviceId = deviceCtx.getDeviceId(); | ||
465 | + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); | ||
466 | + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); | ||
467 | + } catch (Throwable e) { | ||
468 | + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); | ||
469 | + } | ||
470 | + } | ||
471 | + | ||
472 | + @Override | ||
473 | + public void onFailure(Throwable t) { | ||
474 | + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); | ||
475 | + } | ||
476 | + }, context.getExecutor()); | ||
477 | + }); | ||
478 | + } else { | ||
479 | + log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); | ||
480 | + throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); | ||
481 | + } | ||
482 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
483 | + throw new AdaptorException(e); | ||
484 | + } | ||
485 | + } | ||
486 | + | ||
487 | + private void processClaimDeviceMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { | ||
488 | + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); | ||
489 | + } | ||
490 | + | ||
491 | + private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { | ||
492 | + JsonElement json = JsonTcpAdaptor.validateJsonPayload(sessionId, payload); | ||
493 | + if (json.isJsonObject()) { | ||
494 | + JsonObject jsonObj = json.getAsJsonObject(); | ||
495 | + for (Map.Entry<String, JsonElement> deviceEntry : jsonObj.entrySet()) { | ||
496 | + String deviceName = deviceEntry.getKey(); | ||
497 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
498 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
499 | + @Override | ||
500 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
501 | + if (!deviceEntry.getValue().isJsonObject()) { | ||
502 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
503 | + } | ||
504 | + TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); | ||
505 | + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); | ||
506 | + } | ||
507 | + | ||
508 | + @Override | ||
509 | + public void onFailure(Throwable t) { | ||
510 | + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); | ||
511 | + } | ||
512 | + }, context.getExecutor()); | ||
513 | + } | ||
514 | + } else { | ||
515 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
516 | + } | ||
517 | + } | ||
518 | + | ||
519 | + private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { | ||
520 | + try { | ||
521 | + TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); | ||
522 | + List<TransportApiProtos.AttributesMsg> attributesMsgList = attributesMsgProto.getMsgList(); | ||
523 | + if (!CollectionUtils.isEmpty(attributesMsgList)) { | ||
524 | + attributesMsgList.forEach(attributesMsg -> { | ||
525 | + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); | ||
526 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
527 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
528 | + @Override | ||
529 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
530 | + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); | ||
531 | + if (kvListProto == null) { | ||
532 | + throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); | ||
533 | + } | ||
534 | + try { | ||
535 | + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); | ||
536 | + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); | ||
537 | + } catch (Throwable e) { | ||
538 | + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); | ||
539 | + } | ||
540 | + } | ||
541 | + | ||
542 | + @Override | ||
543 | + public void onFailure(Throwable t) { | ||
544 | + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); | ||
545 | + } | ||
546 | + }, context.getExecutor()); | ||
547 | + }); | ||
548 | + } else { | ||
549 | + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); | ||
550 | + throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); | ||
551 | + } | ||
552 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
553 | + throw new AdaptorException(e); | ||
554 | + } | ||
555 | + } | ||
556 | + | ||
557 | + private void processPostAttributesMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { | ||
558 | + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); | ||
559 | + } | ||
560 | + | ||
561 | + private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { | ||
562 | + JsonElement json = JsonTcpAdaptor.validateJsonPayload(sessionId, msg.payload()); | ||
563 | + if (json.isJsonObject()) { | ||
564 | + JsonObject jsonObj = json.getAsJsonObject(); | ||
565 | + int requestId = jsonObj.get("id").getAsInt(); | ||
566 | + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); | ||
567 | + boolean clientScope = jsonObj.get("client").getAsBoolean(); | ||
568 | + Set<String> keys; | ||
569 | + if (jsonObj.has("key")) { | ||
570 | + keys = Collections.singleton(jsonObj.get("key").getAsString()); | ||
571 | + } else { | ||
572 | + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); | ||
573 | + keys = new HashSet<>(); | ||
574 | + for (JsonElement keyObj : keysArray) { | ||
575 | + keys.add(keyObj.getAsString()); | ||
576 | + } | ||
577 | + } | ||
578 | + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); | ||
579 | + processGetAttributeRequestMessage(msg, deviceName, requestMsg); | ||
580 | + } else { | ||
581 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
582 | + } | ||
583 | + } | ||
584 | + | ||
585 | + private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
586 | + try { | ||
587 | + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); | ||
588 | + String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); | ||
589 | + int requestId = gatewayAttributesRequestMsg.getId(); | ||
590 | + boolean clientScope = gatewayAttributesRequestMsg.getClient(); | ||
591 | + ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); | ||
592 | + Set<String> keys = new HashSet<>(keysList); | ||
593 | + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); | ||
594 | + processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); | ||
595 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
596 | + throw new AdaptorException(e); | ||
597 | + } | ||
598 | + } | ||
599 | + | ||
600 | + private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { | ||
601 | + JsonElement json = JsonTcpAdaptor.validateJsonPayload(sessionId, payload); | ||
602 | + if (json.isJsonObject()) { | ||
603 | + JsonObject jsonObj = json.getAsJsonObject(); | ||
604 | + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); | ||
605 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
606 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
607 | + @Override | ||
608 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
609 | + Integer requestId = jsonObj.get("id").getAsInt(); | ||
610 | + String data = jsonObj.get("data").toString(); | ||
611 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() | ||
612 | + .setRequestId(requestId).setPayload(data).build(); | ||
613 | + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); | ||
614 | + } | ||
615 | + | ||
616 | + @Override | ||
617 | + public void onFailure(Throwable t) { | ||
618 | + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); | ||
619 | + } | ||
620 | + }, context.getExecutor()); | ||
621 | + } else { | ||
622 | + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); | ||
623 | + } | ||
624 | + } | ||
625 | + | ||
626 | + private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { | ||
627 | + try { | ||
628 | + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); | ||
629 | + String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); | ||
630 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
631 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
632 | + @Override | ||
633 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
634 | + Integer requestId = gatewayRpcResponseMsg.getId(); | ||
635 | + String data = gatewayRpcResponseMsg.getData(); | ||
636 | + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() | ||
637 | + .setRequestId(requestId).setPayload(data).build(); | ||
638 | + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); | ||
639 | + } | ||
640 | + | ||
641 | + @Override | ||
642 | + public void onFailure(Throwable t) { | ||
643 | + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); | ||
644 | + } | ||
645 | + }, context.getExecutor()); | ||
646 | + } catch (RuntimeException | InvalidProtocolBufferException e) { | ||
647 | + throw new AdaptorException(e); | ||
648 | + } | ||
649 | + } | ||
650 | + | ||
651 | + private void processRpcResponseMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { | ||
652 | + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); | ||
653 | + } | ||
654 | + | ||
655 | + private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { | ||
656 | + int msgId = getMsgId(mqttMsg); | ||
657 | + Futures.addCallback(checkDeviceConnected(deviceName), | ||
658 | + new FutureCallback<GatewayDeviceSessionCtx>() { | ||
659 | + @Override | ||
660 | + public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { | ||
661 | + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); | ||
662 | + } | ||
663 | + | ||
664 | + @Override | ||
665 | + public void onFailure(Throwable t) { | ||
666 | + ack(mqttMsg); | ||
667 | + log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); | ||
668 | + } | ||
669 | + }, context.getExecutor()); | ||
670 | + } | ||
671 | + | ||
672 | + private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set<String> keys) { | ||
673 | + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); | ||
674 | + result.setRequestId(requestId); | ||
675 | + | ||
676 | + if (clientScope) { | ||
677 | + result.addAllClientAttributeNames(keys); | ||
678 | + } else { | ||
679 | + result.addAllSharedAttributeNames(keys); | ||
680 | + } | ||
681 | + return result.build(); | ||
682 | + } | ||
683 | + | ||
684 | + private ListenableFuture<GatewayDeviceSessionCtx> checkDeviceConnected(String deviceName) { | ||
685 | + GatewayDeviceSessionCtx ctx = devices.get(deviceName); | ||
686 | + if (ctx == null) { | ||
687 | + log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); | ||
688 | + return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); | ||
689 | + } else { | ||
690 | + return Futures.immediateFuture(ctx); | ||
691 | + } | ||
692 | + } | ||
693 | + | ||
694 | + private String checkDeviceName(String deviceName) { | ||
695 | + if (StringUtils.isEmpty(deviceName)) { | ||
696 | + throw new RuntimeException("Device name is empty!"); | ||
697 | + } else { | ||
698 | + return deviceName; | ||
699 | + } | ||
700 | + } | ||
701 | + | ||
702 | + private String getDeviceName(JsonElement json) { | ||
703 | + return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); | ||
704 | + } | ||
705 | + | ||
706 | + private String getDeviceType(JsonElement json) { | ||
707 | + JsonElement type = json.getAsJsonObject().get("type"); | ||
708 | + return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); | ||
709 | + } | ||
710 | + | ||
711 | + private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { | ||
712 | + return JsonTcpAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); | ||
713 | + } | ||
714 | + | ||
715 | + private byte[] getBytes(ByteBuf payload) { | ||
716 | + return ProtoTcpAdaptor.toBytes(payload); | ||
717 | + } | ||
718 | + | ||
719 | + private void ack(MqttPublishMessage msg) { | ||
720 | + int msgId = getMsgId(msg); | ||
721 | + if (msgId > 0) { | ||
722 | + writeAndFlush(TcpTransportHandler.createMqttPubAckMsg(msgId)); | ||
723 | + } | ||
724 | + } | ||
725 | + | ||
726 | + private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { | ||
727 | + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); | ||
728 | + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); | ||
729 | + log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); | ||
730 | + } | ||
731 | + | ||
732 | + private <T> TransportServiceCallback<Void> getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { | ||
733 | + return new TransportServiceCallback<Void>() { | ||
734 | + @Override | ||
735 | + public void onSuccess(Void dummy) { | ||
736 | + log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); | ||
737 | + if (msgId > 0) { | ||
738 | + ctx.writeAndFlush(TcpTransportHandler.createMqttPubAckMsg(msgId)); | ||
739 | + } | ||
740 | + } | ||
741 | + | ||
742 | + @Override | ||
743 | + public void onError(Throwable e) { | ||
744 | + log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); | ||
745 | + ctx.close(); | ||
746 | + } | ||
747 | + }; | ||
748 | + } | ||
749 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.session; | ||
17 | + | ||
18 | +import io.netty.handler.codec.mqtt.MqttQoS; | ||
19 | +import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; | ||
20 | + | ||
21 | +import java.util.List; | ||
22 | +import java.util.Map; | ||
23 | +import java.util.UUID; | ||
24 | +import java.util.concurrent.ConcurrentMap; | ||
25 | +import java.util.stream.Collectors; | ||
26 | + | ||
27 | +/** | ||
28 | + * Created by ashvayka on 30.08.18. | ||
29 | + */ | ||
30 | +public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { | ||
31 | + | ||
32 | + private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; | ||
33 | + | ||
34 | + public MqttDeviceAwareSessionContext(UUID sessionId, ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap) { | ||
35 | + super(sessionId); | ||
36 | + this.mqttQoSMap = mqttQoSMap; | ||
37 | + } | ||
38 | + | ||
39 | + public ConcurrentMap<MqttTopicMatcher, Integer> getMqttQoSMap() { | ||
40 | + return mqttQoSMap; | ||
41 | + } | ||
42 | + | ||
43 | + public MqttQoS getQoSForTopic(String topic) { | ||
44 | + List<Integer> qosList = mqttQoSMap.entrySet() | ||
45 | + .stream() | ||
46 | + .filter(entry -> entry.getKey().matches(topic)) | ||
47 | + .map(Map.Entry::getValue) | ||
48 | + .collect(Collectors.toList()); | ||
49 | + if (!qosList.isEmpty()) { | ||
50 | + return MqttQoS.valueOf(qosList.get(0)); | ||
51 | + } else { | ||
52 | + return MqttQoS.AT_LEAST_ONCE; | ||
53 | + } | ||
54 | + } | ||
55 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.session; | ||
17 | + | ||
18 | +import java.util.regex.Pattern; | ||
19 | + | ||
20 | +public class MqttTopicMatcher { | ||
21 | + | ||
22 | + private final String topic; | ||
23 | + private final Pattern topicRegex; | ||
24 | + | ||
25 | + public MqttTopicMatcher(String topic) { | ||
26 | + if(topic == null){ | ||
27 | + throw new NullPointerException("topic"); | ||
28 | + } | ||
29 | + this.topic = topic; | ||
30 | + this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); | ||
31 | + } | ||
32 | + | ||
33 | + public String getTopic() { | ||
34 | + return topic; | ||
35 | + } | ||
36 | + | ||
37 | + public boolean matches(String topic){ | ||
38 | + return this.topicRegex.matcher(topic).matches(); | ||
39 | + } | ||
40 | + | ||
41 | + @Override | ||
42 | + public boolean equals(Object o) { | ||
43 | + if (this == o) return true; | ||
44 | + if (o == null || getClass() != o.getClass()) return false; | ||
45 | + | ||
46 | + MqttTopicMatcher that = (MqttTopicMatcher) o; | ||
47 | + | ||
48 | + return topic.equals(that.topic); | ||
49 | + } | ||
50 | + | ||
51 | + @Override | ||
52 | + public int hashCode() { | ||
53 | + return topic.hashCode(); | ||
54 | + } | ||
55 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.util; | ||
17 | + | ||
18 | +import lombok.Data; | ||
19 | + | ||
20 | +@Data | ||
21 | +public class AlwaysTrueTopicFilter implements TcpTopicFilter { | ||
22 | + | ||
23 | + @Override | ||
24 | + public boolean filter(String topic) { | ||
25 | + return true; | ||
26 | + } | ||
27 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/util/EqualsTopicFilter.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.util; | ||
17 | + | ||
18 | +import lombok.Data; | ||
19 | + | ||
20 | +@Data | ||
21 | +public class EqualsTopicFilter implements TcpTopicFilter { | ||
22 | + | ||
23 | + private final String filter; | ||
24 | + | ||
25 | + @Override | ||
26 | + public boolean filter(String topic) { | ||
27 | + return filter.equals(topic); | ||
28 | + } | ||
29 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.util; | ||
17 | + | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
19 | +import org.thingsboard.server.common.data.device.profile.MqttTopics; | ||
20 | + | ||
21 | +import java.util.concurrent.ConcurrentHashMap; | ||
22 | +import java.util.concurrent.ConcurrentMap; | ||
23 | + | ||
24 | +@Slf4j | ||
25 | +public class MqttTopicFilterFactory { | ||
26 | + | ||
27 | + private static final ConcurrentMap<String, TcpTopicFilter> filters = new ConcurrentHashMap<>(); | ||
28 | + private static final TcpTopicFilter DEFAULT_TELEMETRY_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_TELEMETRY_TOPIC); | ||
29 | + private static final TcpTopicFilter DEFAULT_ATTRIBUTES_TOPIC_FILTER = toFilter(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); | ||
30 | + | ||
31 | + public static TcpTopicFilter toFilter(String topicFilter) { | ||
32 | + if (topicFilter == null || topicFilter.isEmpty()) { | ||
33 | + throw new IllegalArgumentException("Topic filter can't be empty!"); | ||
34 | + } | ||
35 | + return filters.computeIfAbsent(topicFilter, filter -> { | ||
36 | + if (filter.equals("#")) { | ||
37 | + return new AlwaysTrueTopicFilter(); | ||
38 | + } else if (filter.contains("+") || filter.contains("#")) { | ||
39 | + String regex = filter | ||
40 | + .replace("\\", "\\\\") | ||
41 | + .replace("+", "[^/]+") | ||
42 | + .replace("/#", "($|/.*)"); | ||
43 | + log.debug("Converting [{}] to [{}]", filter, regex); | ||
44 | + return new RegexTopicFilter(regex); | ||
45 | + } else { | ||
46 | + return new EqualsTopicFilter(filter); | ||
47 | + } | ||
48 | + }); | ||
49 | + } | ||
50 | + | ||
51 | + public static TcpTopicFilter getDefaultTelemetryFilter() { | ||
52 | + return DEFAULT_TELEMETRY_TOPIC_FILTER; | ||
53 | + } | ||
54 | + | ||
55 | + public static TcpTopicFilter getDefaultAttributesFilter() { | ||
56 | + return DEFAULT_ATTRIBUTES_TOPIC_FILTER; | ||
57 | + } | ||
58 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/util/RegexTopicFilter.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.util; | ||
17 | + | ||
18 | +import lombok.Data; | ||
19 | + | ||
20 | +import java.util.regex.Pattern; | ||
21 | + | ||
22 | +@Data | ||
23 | +public class RegexTopicFilter implements TcpTopicFilter { | ||
24 | + | ||
25 | + private final Pattern regex; | ||
26 | + | ||
27 | + public RegexTopicFilter(String regex) { | ||
28 | + this.regex = Pattern.compile(regex); | ||
29 | + } | ||
30 | + | ||
31 | + @Override | ||
32 | + public boolean filter(String topic) { | ||
33 | + return regex.matcher(topic).matches(); | ||
34 | + } | ||
35 | +} |
common/transport/tcp/src/main/java/org/thingsboard/server/transport/tcp/util/TcpTopicFilter.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.tcp.util; | ||
17 | + | ||
18 | +public interface TcpTopicFilter { | ||
19 | + | ||
20 | + boolean filter(String topic); | ||
21 | + | ||
22 | +} |
@@ -939,6 +939,11 @@ | @@ -939,6 +939,11 @@ | ||
939 | </dependency> | 939 | </dependency> |
940 | <dependency> | 940 | <dependency> |
941 | <groupId>org.thingsboard.common.transport</groupId> | 941 | <groupId>org.thingsboard.common.transport</groupId> |
942 | + <artifactId>tcp</artifactId> | ||
943 | + <version>${project.version}</version> | ||
944 | + </dependency> | ||
945 | + <dependency> | ||
946 | + <groupId>org.thingsboard.common.transport</groupId> | ||
942 | <artifactId>http</artifactId> | 947 | <artifactId>http</artifactId> |
943 | <version>${project.version}</version> | 948 | <version>${project.version}</version> |
944 | </dependency> | 949 | </dependency> |
@@ -41,7 +41,7 @@ | @@ -41,7 +41,7 @@ | ||
41 | <pkg.copyInstallScripts>false</pkg.copyInstallScripts> | 41 | <pkg.copyInstallScripts>false</pkg.copyInstallScripts> |
42 | <pkg.win.dist>${project.build.directory}/windows</pkg.win.dist> | 42 | <pkg.win.dist>${project.build.directory}/windows</pkg.win.dist> |
43 | <pkg.implementationTitle>ThingsBoard MQTT Transport Service</pkg.implementationTitle> | 43 | <pkg.implementationTitle>ThingsBoard MQTT Transport Service</pkg.implementationTitle> |
44 | - <pkg.mainClass>org.thingsboard.server.mqtt.ThingsboardMqttTransportApplication</pkg.mainClass> | 44 | + <pkg.mainClass>org.thingsboard.server.tcp.ThingsboardMqttTransportApplication</pkg.mainClass> |
45 | </properties> | 45 | </properties> |
46 | 46 | ||
47 | <dependencies> | 47 | <dependencies> |
@@ -36,6 +36,7 @@ | @@ -36,6 +36,7 @@ | ||
36 | <modules> | 36 | <modules> |
37 | <module>http</module> | 37 | <module>http</module> |
38 | <module>mqtt</module> | 38 | <module>mqtt</module> |
39 | + <module>tcp</module> | ||
39 | <module>coap</module> | 40 | <module>coap</module> |
40 | <module>lwm2m</module> | 41 | <module>lwm2m</module> |
41 | <module>snmp</module> | 42 | <module>snmp</module> |
transport/tcp/pom.xml
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2022 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
19 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
20 | + <modelVersion>4.0.0</modelVersion> | ||
21 | + <parent> | ||
22 | + <groupId>org.thingsboard</groupId> | ||
23 | + <version>3.3.4-SNAPSHOT</version> | ||
24 | + <artifactId>transport</artifactId> | ||
25 | + </parent> | ||
26 | + <groupId>org.thingsboard.transport</groupId> | ||
27 | + <artifactId>tcp</artifactId> | ||
28 | + <packaging>jar</packaging> | ||
29 | + | ||
30 | + <name>Thingsboard TCP Transport Service</name> | ||
31 | + <url>https://thingsboard.io</url> | ||
32 | + | ||
33 | + <properties> | ||
34 | + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
35 | + <main.dir>${basedir}/../..</main.dir> | ||
36 | + <pkg.type>java</pkg.type> | ||
37 | + <pkg.disabled>false</pkg.disabled> | ||
38 | + <pkg.process-resources.phase>process-resources</pkg.process-resources.phase> | ||
39 | + <pkg.package.phase>package</pkg.package.phase> | ||
40 | + <pkg.name>tb-tcp-transport</pkg.name> | ||
41 | + <pkg.copyInstallScripts>false</pkg.copyInstallScripts> | ||
42 | + <pkg.win.dist>${project.build.directory}/windows</pkg.win.dist> | ||
43 | + <pkg.implementationTitle>ThingsBoard TCP Transport Service</pkg.implementationTitle> | ||
44 | + <pkg.mainClass>org.thingsboard.server.tcp.ThingsboardTcpTransportApplication</pkg.mainClass> | ||
45 | + </properties> | ||
46 | + | ||
47 | + <dependencies> | ||
48 | + <dependency> | ||
49 | + <groupId>org.thingsboard.common.transport</groupId> | ||
50 | + <artifactId>tcp</artifactId> | ||
51 | + </dependency> | ||
52 | + <dependency> | ||
53 | + <groupId>org.thingsboard.common</groupId> | ||
54 | + <artifactId>queue</artifactId> | ||
55 | + </dependency> | ||
56 | + <dependency> | ||
57 | + <groupId>org.springframework.boot</groupId> | ||
58 | + <artifactId>spring-boot-starter-web</artifactId> | ||
59 | + </dependency> | ||
60 | + <dependency> | ||
61 | + <groupId>com.sun.winsw</groupId> | ||
62 | + <artifactId>winsw</artifactId> | ||
63 | + <classifier>bin</classifier> | ||
64 | + <type>exe</type> | ||
65 | + <scope>provided</scope> | ||
66 | + </dependency> | ||
67 | + <dependency> | ||
68 | + <groupId>org.springframework.boot</groupId> | ||
69 | + <artifactId>spring-boot-starter-test</artifactId> | ||
70 | + <scope>test</scope> | ||
71 | + </dependency> | ||
72 | + <dependency> | ||
73 | + <groupId>org.junit.vintage</groupId> | ||
74 | + <artifactId>junit-vintage-engine</artifactId> | ||
75 | + <scope>test</scope> | ||
76 | + </dependency> | ||
77 | + <dependency> | ||
78 | + <groupId>org.awaitility</groupId> | ||
79 | + <artifactId>awaitility</artifactId> | ||
80 | + <scope>test</scope> | ||
81 | + </dependency> | ||
82 | + </dependencies> | ||
83 | + | ||
84 | + <build> | ||
85 | + <finalName>${pkg.name}-${project.version}</finalName> | ||
86 | + <resources> | ||
87 | + <resource> | ||
88 | + <directory>${project.basedir}/src/main/resources</directory> | ||
89 | + </resource> | ||
90 | + </resources> | ||
91 | + <plugins> | ||
92 | + <plugin> | ||
93 | + <groupId>org.apache.maven.plugins</groupId> | ||
94 | + <artifactId>maven-resources-plugin</artifactId> | ||
95 | + </plugin> | ||
96 | + <plugin> | ||
97 | + <groupId>org.apache.maven.plugins</groupId> | ||
98 | + <artifactId>maven-dependency-plugin</artifactId> | ||
99 | + </plugin> | ||
100 | + <plugin> | ||
101 | + <groupId>org.apache.maven.plugins</groupId> | ||
102 | + <artifactId>maven-jar-plugin</artifactId> | ||
103 | + </plugin> | ||
104 | + <plugin> | ||
105 | + <groupId>org.springframework.boot</groupId> | ||
106 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
107 | + </plugin> | ||
108 | + <plugin> | ||
109 | + <groupId>org.thingsboard</groupId> | ||
110 | + <artifactId>gradle-maven-plugin</artifactId> | ||
111 | + </plugin> | ||
112 | + <plugin> | ||
113 | + <groupId>org.apache.maven.plugins</groupId> | ||
114 | + <artifactId>maven-assembly-plugin</artifactId> | ||
115 | + </plugin> | ||
116 | + <plugin> | ||
117 | + <groupId>org.apache.maven.plugins</groupId> | ||
118 | + <artifactId>maven-install-plugin</artifactId> | ||
119 | + </plugin> | ||
120 | + </plugins> | ||
121 | + </build> | ||
122 | + <repositories> | ||
123 | + <repository> | ||
124 | + <id>jenkins</id> | ||
125 | + <name>Jenkins Repository</name> | ||
126 | + <url>https://repo.jenkins-ci.org/releases</url> | ||
127 | + <snapshots> | ||
128 | + <enabled>false</enabled> | ||
129 | + </snapshots> | ||
130 | + </repository> | ||
131 | + </repositories> | ||
132 | +</project> |
transport/tcp/src/main/conf/logback.xml
0 → 100644
1 | +<?xml version="1.0" encoding="UTF-8" ?> | ||
2 | +<!-- | ||
3 | + | ||
4 | + Copyright © 2016-2022 The Thingsboard Authors | ||
5 | + | ||
6 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
7 | + you may not use this file except in compliance with the License. | ||
8 | + You may obtain a copy of the License at | ||
9 | + | ||
10 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
11 | + | ||
12 | + Unless required by applicable law or agreed to in writing, software | ||
13 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
14 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
15 | + See the License for the specific language governing permissions and | ||
16 | + limitations under the License. | ||
17 | + | ||
18 | +--> | ||
19 | +<!DOCTYPE configuration> | ||
20 | +<configuration> | ||
21 | + | ||
22 | + <appender name="fileLogAppender" | ||
23 | + class="ch.qos.logback.core.rolling.RollingFileAppender"> | ||
24 | + <file>${pkg.logFolder}/${pkg.name}.log</file> | ||
25 | + <rollingPolicy | ||
26 | + class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> | ||
27 | + <fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> | ||
28 | + <maxFileSize>100MB</maxFileSize> | ||
29 | + <maxHistory>30</maxHistory> | ||
30 | + <totalSizeCap>3GB</totalSizeCap> | ||
31 | + </rollingPolicy> | ||
32 | + <encoder> | ||
33 | + <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||
34 | + </encoder> | ||
35 | + </appender> | ||
36 | + | ||
37 | + <logger name="org.thingsboard.server" level="INFO" /> | ||
38 | + | ||
39 | + <logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> | ||
40 | + | ||
41 | + <root level="INFO"> | ||
42 | + <appender-ref ref="fileLogAppender"/> | ||
43 | + </root> | ||
44 | + | ||
45 | +</configuration> |
1 | +# | ||
2 | +# Copyright © 2016-2022 The Thingsboard Authors | ||
3 | +# | ||
4 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +# you may not use this file except in compliance with the License. | ||
6 | +# You may obtain a copy of the License at | ||
7 | +# | ||
8 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +# | ||
10 | +# Unless required by applicable law or agreed to in writing, software | ||
11 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +# See the License for the specific language governing permissions and | ||
14 | +# limitations under the License. | ||
15 | +# | ||
16 | + | ||
17 | +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" | ||
18 | +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" | ||
19 | +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" | ||
20 | +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" | ||
21 | +export LOG_FILENAME=${pkg.name}.out | ||
22 | +export LOADER_PATH=${pkg.installFolder}/conf |
transport/tcp/src/main/java/org/thingsboard/server/tcp/ThingsboardTcpTransportApplication.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2022 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.tcp; | ||
17 | + | ||
18 | +import org.springframework.boot.SpringApplication; | ||
19 | +import org.springframework.boot.SpringBootConfiguration; | ||
20 | +import org.springframework.context.annotation.ComponentScan; | ||
21 | +import org.springframework.scheduling.annotation.EnableAsync; | ||
22 | +import org.springframework.scheduling.annotation.EnableScheduling; | ||
23 | + | ||
24 | +import java.util.Arrays; | ||
25 | + | ||
26 | +@SpringBootConfiguration | ||
27 | +@EnableAsync | ||
28 | +@EnableScheduling | ||
29 | +@ComponentScan({"org.thingsboard.server.tcp", "org.thingsboard.server.common", "org.thingsboard.server.transport.tcp", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) | ||
30 | +public class ThingsboardTcpTransportApplication { | ||
31 | + | ||
32 | + private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; | ||
33 | + private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "tb-tcp-transport"; | ||
34 | + | ||
35 | + public static void main(String[] args) { | ||
36 | + SpringApplication.run(ThingsboardTcpTransportApplication.class, updateArguments(args)); | ||
37 | + } | ||
38 | + | ||
39 | + private static String[] updateArguments(String[] args) { | ||
40 | + if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { | ||
41 | + String[] modifiedArgs = new String[args.length + 1]; | ||
42 | + System.arraycopy(args, 0, modifiedArgs, 0, args.length); | ||
43 | + modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM; | ||
44 | + return modifiedArgs; | ||
45 | + } | ||
46 | + return args; | ||
47 | + } | ||
48 | +} |
transport/tcp/src/main/resources/logback.xml
0 → 100644
1 | +<?xml version="1.0" encoding="UTF-8" ?> | ||
2 | +<!-- | ||
3 | + | ||
4 | + Copyright © 2016-2022 The Thingsboard Authors | ||
5 | + | ||
6 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
7 | + you may not use this file except in compliance with the License. | ||
8 | + You may obtain a copy of the License at | ||
9 | + | ||
10 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
11 | + | ||
12 | + Unless required by applicable law or agreed to in writing, software | ||
13 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
14 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
15 | + See the License for the specific language governing permissions and | ||
16 | + limitations under the License. | ||
17 | + | ||
18 | +--> | ||
19 | +<!DOCTYPE configuration> | ||
20 | +<configuration scan="true" scanPeriod="10 seconds"> | ||
21 | + | ||
22 | + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | ||
23 | + <encoder> | ||
24 | + <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern> | ||
25 | + </encoder> | ||
26 | + </appender> | ||
27 | + | ||
28 | + <logger name="org.thingsboard.server" level="TRACE" /> | ||
29 | + | ||
30 | + <logger name="com.microsoft.azure.servicebus.primitives.CoreMessageReceiver" level="OFF" /> | ||
31 | + | ||
32 | + <root level="INFO"> | ||
33 | + <appender-ref ref="STDOUT"/> | ||
34 | + </root> | ||
35 | + | ||
36 | +</configuration> |
1 | +# | ||
2 | +# Copyright © 2016-2022 The Thingsboard Authors | ||
3 | +# | ||
4 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +# you may not use this file except in compliance with the License. | ||
6 | +# You may obtain a copy of the License at | ||
7 | +# | ||
8 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +# | ||
10 | +# Unless required by applicable law or agreed to in writing, software | ||
11 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +# See the License for the specific language governing permissions and | ||
14 | +# limitations under the License. | ||
15 | +# | ||
16 | + | ||
17 | +# If you enabled process metrics you should also enable 'web-environment'. | ||
18 | +spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" | ||
19 | +# If you enabled process metrics you should set 'web-application-type' to 'servlet' value. | ||
20 | +spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" | ||
21 | + | ||
22 | +server: | ||
23 | + # Server bind address (has no effect if web-environment is disabled). | ||
24 | + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" | ||
25 | + # Server bind port (has no effect if web-environment is disabled). | ||
26 | + port: "${HTTP_BIND_PORT:8083}" | ||
27 | + | ||
28 | +# Zookeeper connection parameters. Used for service discovery. | ||
29 | +zk: | ||
30 | + # Enable/disable zookeeper discovery service. | ||
31 | + enabled: "${ZOOKEEPER_ENABLED:false}" | ||
32 | + # Zookeeper connect string | ||
33 | + url: "${ZOOKEEPER_URL:localhost:2181}" | ||
34 | + # Zookeeper retry interval in milliseconds | ||
35 | + retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" | ||
36 | + # Zookeeper connection timeout in milliseconds | ||
37 | + connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" | ||
38 | + # Zookeeper session timeout in milliseconds | ||
39 | + session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" | ||
40 | + # Name of the directory in zookeeper 'filesystem' | ||
41 | + zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" | ||
42 | + | ||
43 | +cache: | ||
44 | + type: "${CACHE_TYPE:redis}" | ||
45 | + | ||
46 | +redis: | ||
47 | + # standalone or cluster | ||
48 | + connection: | ||
49 | + type: "${REDIS_CONNECTION_TYPE:standalone}" | ||
50 | + standalone: | ||
51 | + host: "${REDIS_HOST:localhost}" | ||
52 | + port: "${REDIS_PORT:6379}" | ||
53 | + useDefaultClientConfig: "${REDIS_USE_DEFAULT_CLIENT_CONFIG:true}" | ||
54 | + # this value may be used only if you used not default ClientConfig | ||
55 | + clientName: "${REDIS_CLIENT_NAME:standalone}" | ||
56 | + # this value may be used only if you used not default ClientConfig | ||
57 | + connectTimeout: "${REDIS_CLIENT_CONNECT_TIMEOUT:30000}" | ||
58 | + # this value may be used only if you used not default ClientConfig | ||
59 | + readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}" | ||
60 | + # this value may be used only if you used not default ClientConfig | ||
61 | + usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:false}" | ||
62 | + cluster: | ||
63 | + # Comma-separated list of "host:port" pairs to bootstrap from. | ||
64 | + nodes: "${REDIS_NODES:}" | ||
65 | + # Maximum number of redirects to follow when executing commands across the cluster. | ||
66 | + max-redirects: "${REDIS_MAX_REDIRECTS:12}" | ||
67 | + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" | ||
68 | + # db index | ||
69 | + db: "${REDIS_DB:0}" | ||
70 | + # db password | ||
71 | + password: "${REDIS_PASSWORD:}" | ||
72 | + # pool config | ||
73 | + pool_config: | ||
74 | + maxTotal: "${REDIS_POOL_CONFIG_MAX_TOTAL:128}" | ||
75 | + maxIdle: "${REDIS_POOL_CONFIG_MAX_IDLE:128}" | ||
76 | + minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" | ||
77 | + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" | ||
78 | + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" | ||
79 | + testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" | ||
80 | + minEvictableMs: "${REDIS_POOL_CONFIG_MIN_EVICTABLE_MS:60000}" | ||
81 | + evictionRunsMs: "${REDIS_POOL_CONFIG_EVICTION_RUNS_MS:30000}" | ||
82 | + maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}" | ||
83 | + numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}" | ||
84 | + blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" | ||
85 | + | ||
86 | +# TCP_ server parameters | ||
87 | +transport: | ||
88 | + tcp: | ||
89 | + bind_address: "${TCP_BIND_ADDRESS:0.0.0.0}" | ||
90 | + bind_port: "${TCP_BIND_PORT:8088}" | ||
91 | + # Enable proxy protocol support. Disabled by default. If enabled, supports both v1 and v2. | ||
92 | + # Useful to get the real IP address of the client in the logs and for rate limits. | ||
93 | + proxy_enabled: "${TCP_PROXY_PROTOCOL_ENABLED:false}" | ||
94 | + timeout: "${TCP_TIMEOUT:10000}" | ||
95 | + msg_queue_size_per_device_limit: "${TCP_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | ||
96 | + netty: | ||
97 | + leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" | ||
98 | + boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | ||
99 | + worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}" | ||
100 | + max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}" | ||
101 | + so_keep_alive: "${NETTY_SO_KEEPALIVE:false}" | ||
102 | + # TCP_ SSL configuration | ||
103 | + ssl: | ||
104 | + # Enable/disable SSL support | ||
105 | + enabled: "${TCP_SSL_ENABLED:false}" | ||
106 | + # TCP_ SSL bind address | ||
107 | + bind_address: "${TCP_SSL_BIND_ADDRESS:0.0.0.0}" | ||
108 | + # TCP_ SSL bind port | ||
109 | + bind_port: "${TCP_SSL_BIND_PORT:8883}" | ||
110 | + # SSL protocol: See https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms | ||
111 | + protocol: "${TCP_SSL_PROTOCOL:TLSv1.2}" | ||
112 | + # Server SSL credentials | ||
113 | + credentials: | ||
114 | + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) | ||
115 | + type: "${TCP_SSL_CREDENTIALS_TYPE:PEM}" | ||
116 | + # PEM server credentials | ||
117 | + pem: | ||
118 | + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) | ||
119 | + cert_file: "${TCP_SSL_PEM_CERT:tcpserver.pem}" | ||
120 | + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; | ||
121 | + key_file: "${TCP_SSL_PEM_KEY:tcpserver_key.pem}" | ||
122 | + # Server certificate private key password (optional) | ||
123 | + key_password: "${TCP_SSL_PEM_KEY_PASSWORD:server_key_password}" | ||
124 | + # Keystore server credentials | ||
125 | + keystore: | ||
126 | + # Type of the key store | ||
127 | + type: "${TCP_SSL_KEY_STORE_TYPE:JKS}" | ||
128 | + # Path to the key store that holds the SSL certificate | ||
129 | + store_file: "${TCP_SSL_KEY_STORE:tcpserver.jks}" | ||
130 | + # Password used to access the key store | ||
131 | + store_password: "${TCP_SSL_KEY_STORE_PASSWORD:server_ks_password}" | ||
132 | + # Optional alias of the private key; If not set, the platform will load the first private key from the keystore; | ||
133 | + key_alias: "${TCP_SSL_KEY_ALIAS:}" | ||
134 | + # Password used to access the key | ||
135 | + key_password: "${TCP_SSL_KEY_PASSWORD:server_key_password}" | ||
136 | + # Skip certificate validity check for client certificates. | ||
137 | + skip_validity_check_for_client_cert: "${TCP_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" | ||
138 | + sessions: | ||
139 | + inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" | ||
140 | + report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}" | ||
141 | + json: | ||
142 | + # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON | ||
143 | + type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" | ||
144 | + # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) | ||
145 | + max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" | ||
146 | + log: | ||
147 | + enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" | ||
148 | + max_length: "${TB_TRANSPORT_LOG_MAX_LENGTH:1024}" | ||
149 | + stats: | ||
150 | + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" | ||
151 | + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" | ||
152 | + client_side_rpc: | ||
153 | + timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" | ||
154 | + rate_limits: | ||
155 | + # Enable or disable generic rate limits. Device and Tenant specific rate limits are controlled in Tenant Profile. | ||
156 | + ip_limits_enabled: "${TB_TRANSPORT_IP_RATE_LIMITS_ENABLED:false}" | ||
157 | + # Maximum number of connect attempts with invalid credentials | ||
158 | + max_wrong_credentials_per_ip: "${TB_TRANSPORT_MAX_WRONG_CREDENTIALS_PER_IP:10}" | ||
159 | + # Timeout to expire block IP addresses | ||
160 | + ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" | ||
161 | + | ||
162 | + | ||
163 | +queue: | ||
164 | + type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) | ||
165 | + kafka: | ||
166 | + bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" | ||
167 | + acks: "${TB_KAFKA_ACKS:all}" | ||
168 | + retries: "${TB_KAFKA_RETRIES:1}" | ||
169 | + batch.size: "${TB_KAFKA_BATCH_SIZE:16384}" | ||
170 | + linger.ms: "${TB_KAFKA_LINGER_MS:1}" | ||
171 | + max.request.size: "${TB_KAFKA_MAX_REQUEST_SIZE:1048576}" | ||
172 | + max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" | ||
173 | + buffer.memory: "${TB_BUFFER_MEMORY:33554432}" | ||
174 | + replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" | ||
175 | + use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" | ||
176 | + confluent: | ||
177 | + ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" | ||
178 | + sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" | ||
179 | + sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" | ||
180 | + security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" | ||
181 | + other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside | ||
182 | + - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms | ||
183 | + value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) | ||
184 | + - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms | ||
185 | + value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) | ||
186 | + topic-properties: | ||
187 | + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" | ||
188 | + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" | ||
189 | + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" | ||
190 | + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" | ||
191 | + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" | ||
192 | + aws_sqs: | ||
193 | + use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" | ||
194 | + access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" | ||
195 | + secret_access_key: "${TB_QUEUE_AWS_SQS_SECRET_ACCESS_KEY:YOUR_SECRET}" | ||
196 | + region: "${TB_QUEUE_AWS_SQS_REGION:YOUR_REGION}" | ||
197 | + threads_per_topic: "${TB_QUEUE_AWS_SQS_THREADS_PER_TOPIC:1}" | ||
198 | + queue-properties: | ||
199 | + rule-engine: "${TB_QUEUE_AWS_SQS_RE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" | ||
200 | + core: "${TB_QUEUE_AWS_SQS_CORE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" | ||
201 | + transport-api: "${TB_QUEUE_AWS_SQS_TA_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" | ||
202 | + notifications: "${TB_QUEUE_AWS_SQS_NOTIFICATIONS_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" | ||
203 | + js-executor: "${TB_QUEUE_AWS_SQS_JE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" | ||
204 | + # VisibilityTimeout in seconds;MaximumMessageSize in bytes;MessageRetentionPeriod in seconds | ||
205 | + pubsub: | ||
206 | + project_id: "${TB_QUEUE_PUBSUB_PROJECT_ID:YOUR_PROJECT_ID}" | ||
207 | + service_account: "${TB_QUEUE_PUBSUB_SERVICE_ACCOUNT:YOUR_SERVICE_ACCOUNT}" | ||
208 | + max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" #in bytes | ||
209 | + max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" | ||
210 | + queue-properties: | ||
211 | + rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" | ||
212 | + core: "${TB_QUEUE_PUBSUB_CORE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" | ||
213 | + transport-api: "${TB_QUEUE_PUBSUB_TA_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" | ||
214 | + notifications: "${TB_QUEUE_PUBSUB_NOTIFICATIONS_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" | ||
215 | + js-executor: "${TB_QUEUE_PUBSUB_JE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" | ||
216 | + service_bus: | ||
217 | + namespace_name: "${TB_QUEUE_SERVICE_BUS_NAMESPACE_NAME:YOUR_NAMESPACE_NAME}" | ||
218 | + sas_key_name: "${TB_QUEUE_SERVICE_BUS_SAS_KEY_NAME:YOUR_SAS_KEY_NAME}" | ||
219 | + sas_key: "${TB_QUEUE_SERVICE_BUS_SAS_KEY:YOUR_SAS_KEY}" | ||
220 | + max_messages: "${TB_QUEUE_SERVICE_BUS_MAX_MESSAGES:1000}" | ||
221 | + queue-properties: | ||
222 | + rule-engine: "${TB_QUEUE_SERVICE_BUS_RE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" | ||
223 | + core: "${TB_QUEUE_SERVICE_BUS_CORE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" | ||
224 | + transport-api: "${TB_QUEUE_SERVICE_BUS_TA_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" | ||
225 | + notifications: "${TB_QUEUE_SERVICE_BUS_NOTIFICATIONS_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" | ||
226 | + js-executor: "${TB_QUEUE_SERVICE_BUS_JE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" | ||
227 | + rabbitmq: | ||
228 | + exchange_name: "${TB_QUEUE_RABBIT_MQ_EXCHANGE_NAME:}" | ||
229 | + host: "${TB_QUEUE_RABBIT_MQ_HOST:localhost}" | ||
230 | + port: "${TB_QUEUE_RABBIT_MQ_PORT:5672}" | ||
231 | + virtual_host: "${TB_QUEUE_RABBIT_MQ_VIRTUAL_HOST:/}" | ||
232 | + username: "${TB_QUEUE_RABBIT_MQ_USERNAME:YOUR_USERNAME}" | ||
233 | + password: "${TB_QUEUE_RABBIT_MQ_PASSWORD:YOUR_PASSWORD}" | ||
234 | + automatic_recovery_enabled: "${TB_QUEUE_RABBIT_MQ_AUTOMATIC_RECOVERY_ENABLED:false}" | ||
235 | + connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}" | ||
236 | + handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}" | ||
237 | + queue-properties: | ||
238 | + rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" | ||
239 | + core: "${TB_QUEUE_RABBIT_MQ_CORE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" | ||
240 | + transport-api: "${TB_QUEUE_RABBIT_MQ_TA_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" | ||
241 | + notifications: "${TB_QUEUE_RABBIT_MQ_NOTIFICATIONS_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" | ||
242 | + js-executor: "${TB_QUEUE_RABBIT_MQ_JE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" | ||
243 | + partitions: | ||
244 | + hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" | ||
245 | + virtual_nodes_size: "${TB_QUEUE_PARTITIONS_VIRTUAL_NODES_SIZE:16}" | ||
246 | + transport_api: | ||
247 | + requests_topic: "${TB_QUEUE_TRANSPORT_API_REQUEST_TOPIC:tb_transport.api.requests}" | ||
248 | + responses_topic: "${TB_QUEUE_TRANSPORT_API_RESPONSE_TOPIC:tb_transport.api.responses}" | ||
249 | + max_pending_requests: "${TB_QUEUE_TRANSPORT_MAX_PENDING_REQUESTS:10000}" | ||
250 | + max_requests_timeout: "${TB_QUEUE_TRANSPORT_MAX_REQUEST_TIMEOUT:10000}" | ||
251 | + max_callback_threads: "${TB_QUEUE_TRANSPORT_MAX_CALLBACK_THREADS:100}" | ||
252 | + request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" | ||
253 | + response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" | ||
254 | + core: | ||
255 | + topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" | ||
256 | + poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" | ||
257 | + partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" | ||
258 | + pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}" | ||
259 | + usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" | ||
260 | + stats: | ||
261 | + enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" | ||
262 | + print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" | ||
263 | + js: | ||
264 | + # JS Eval request topic | ||
265 | + request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" | ||
266 | + # JS Eval responses topic prefix that is combined with node id | ||
267 | + response_topic_prefix: "${REMOTE_JS_EVAL_RESPONSE_TOPIC:js_eval.responses}" | ||
268 | + # JS Eval max pending requests | ||
269 | + max_pending_requests: "${REMOTE_JS_MAX_PENDING_REQUESTS:10000}" | ||
270 | + # JS Eval max request timeout | ||
271 | + max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" | ||
272 | + # JS response poll interval | ||
273 | + response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" | ||
274 | + rule-engine: | ||
275 | + topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" | ||
276 | + poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" | ||
277 | + pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:60000}" | ||
278 | + stats: | ||
279 | + enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" | ||
280 | + print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" | ||
281 | + queues: | ||
282 | + - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" | ||
283 | + topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" | ||
284 | + poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" | ||
285 | + partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" | ||
286 | + pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" | ||
287 | + submit-strategy: | ||
288 | + type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL | ||
289 | + # For BATCH only | ||
290 | + batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch | ||
291 | + processing-strategy: | ||
292 | + type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
293 | + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
294 | + retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited | ||
295 | + failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; | ||
296 | + pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; | ||
297 | + - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" | ||
298 | + topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" | ||
299 | + poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" | ||
300 | + partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" | ||
301 | + pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" | ||
302 | + submit-strategy: | ||
303 | + type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL | ||
304 | + # For BATCH only | ||
305 | + batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch | ||
306 | + processing-strategy: | ||
307 | + type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
308 | + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
309 | + retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited | ||
310 | + failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; | ||
311 | + pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; | ||
312 | + - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" | ||
313 | + topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" | ||
314 | + poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" | ||
315 | + partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" | ||
316 | + pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" | ||
317 | + submit-strategy: | ||
318 | + type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL | ||
319 | + # For BATCH only | ||
320 | + batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch | ||
321 | + processing-strategy: | ||
322 | + type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
323 | + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT | ||
324 | + retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited | ||
325 | + failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; | ||
326 | + pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; | ||
327 | + transport: | ||
328 | + # For high priority notifications that require minimum latency and processing time | ||
329 | + notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" | ||
330 | + poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" | ||
331 | + | ||
332 | +service: | ||
333 | + type: "${TB_SERVICE_TYPE:tb-transport}" | ||
334 | + # Unique id for this service (autogenerated if empty) | ||
335 | + id: "${TB_SERVICE_ID:}" | ||
336 | + tenant_id: "${TB_SERVICE_TENANT_ID:}" # empty or specific tenant id. | ||
337 | + | ||
338 | +metrics: | ||
339 | + # Enable/disable actuator metrics. | ||
340 | + enabled: "${METRICS_ENABLED:false}" | ||
341 | + | ||
342 | +management: | ||
343 | + endpoints: | ||
344 | + web: | ||
345 | + exposure: | ||
346 | + # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). | ||
347 | + include: '${METRICS_ENDPOINTS_EXPOSE:info}' |