00_MQTT2_SERVER.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. ##############################################
  2. # $Id: 00_MQTT2_SERVER.pm 17702 2018-11-07 19:02:28Z rudolfkoenig $
  3. package main;
  4. # TODO: test SSL
  5. use strict;
  6. use warnings;
  7. use TcpServerUtils;
  8. use MIME::Base64;
  9. sub MQTT2_SERVER_Read($@);
  10. sub MQTT2_SERVER_Write($$$);
  11. sub MQTT2_SERVER_Undef($@);
  12. sub MQTT2_SERVER_doPublish($$$$;$);
  13. # See also:
  14. # http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
  15. sub
  16. MQTT2_SERVER_Initialize($)
  17. {
  18. my ($hash) = @_;
  19. $hash->{Clients} = ":MQTT2_DEVICE:";
  20. $hash->{MatchList}= { "1:MQTT2_DEVICE" => "^.*" },
  21. $hash->{ReadFn} = "MQTT2_SERVER_Read";
  22. $hash->{DefFn} = "MQTT2_SERVER_Define";
  23. $hash->{AttrFn} = "MQTT2_SERVER_Attr";
  24. $hash->{SetFn} = "MQTT2_SERVER_Set";
  25. $hash->{UndefFn} = "MQTT2_SERVER_Undef";
  26. $hash->{WriteFn} = "MQTT2_SERVER_Write";
  27. $hash->{StateFn} = "MQTT2_SERVER_State";
  28. $hash->{CanAuthenticate} = 1;
  29. no warnings 'qw';
  30. my @attrList = qw(
  31. SSL:0,1
  32. autocreate
  33. disable:0,1
  34. disabledForIntervals
  35. rawEvents
  36. sslVersion
  37. sslCertPrefix
  38. );
  39. use warnings 'qw';
  40. $hash->{AttrList} = join(" ", @attrList);
  41. }
  42. #####################################
  43. sub
  44. MQTT2_SERVER_Define($$)
  45. {
  46. my ($hash, $def) = @_;
  47. my ($name, $type, $port, $global) = split("[ \t]+", $def);
  48. return "Usage: define <name> MQTT2_SERVER [IPV6:]<tcp-portnr> [global]"
  49. if($port !~ m/^(IPV6:)?\d+$/);
  50. MQTT2_SERVER_Undef($hash, undef) if($hash->{OLDDEF}); # modify
  51. my $ret = TcpServer_Open($hash, $port, $global);
  52. # Make sure that fhem only runs once
  53. if($ret && !$init_done) {
  54. Log3 $hash, 1, "$ret. Exiting.";
  55. exit(1);
  56. }
  57. readingsSingleUpdate($hash, "nrclients", 0, 0);
  58. $hash->{clients} = {};
  59. $hash->{retain} = {};
  60. InternalTimer(1, "MQTT2_SERVER_keepaliveChecker", $hash, 0);
  61. return $ret;
  62. }
  63. sub
  64. MQTT2_SERVER_keepaliveChecker($)
  65. {
  66. my ($hash) = @_;
  67. my $now = gettimeofday();
  68. my $multiplier = AttrVal($hash, "keepaliveFactor", 1.5);
  69. if($multiplier) {
  70. foreach my $clName (keys %{$hash->{clients}}) {
  71. my $cHash = $defs{$clName};
  72. next if(!$cHash || !$cHash->{keepalive} ||
  73. $now < $cHash->{lastMsgTime}+$cHash->{keepalive}*$multiplier );
  74. Log3 $hash, 3, "$hash->{NAME}: $clName left us (keepalive check)";
  75. CommandDelete(undef, $clName);
  76. }
  77. }
  78. InternalTimer($now+10, "MQTT2_SERVER_keepaliveChecker", $hash, 0);
  79. }
  80. sub
  81. MQTT2_SERVER_Undef($@)
  82. {
  83. my ($hash, $arg) = @_;
  84. my $ret = TcpServer_Close($hash);
  85. my $sname = $hash->{SNAME};
  86. return undef if(!$sname);
  87. my $shash = $defs{$sname};
  88. delete($shash->{clients}{$hash->{NAME}});
  89. readingsSingleUpdate($shash, "nrclients",
  90. ReadingsVal($sname, "nrclients", 1)-1, 1);
  91. if($hash->{lwt}) { # Last will
  92. # skip lwt if there is another connection with the same ip+cid (tasmota??)
  93. for my $dev (keys %defs) {
  94. my $h = $defs{$dev};
  95. next if($h->{TYPE} ne $hash->{TYPE} ||
  96. $h->{NR} == $hash->{NR} ||
  97. !$h->{cid} || $h->{cid} ne $hash->{cid} ||
  98. !$h->{PEER} || $h->{PEER} ne $hash->{PEER});
  99. Log3 $shash, 4,
  100. "Closing second connection for $h->{cid}/$h->{PEER} without lwt";
  101. return $ret;
  102. }
  103. my ($tp, $val) = split(':', $hash->{lwt}, 2);
  104. MQTT2_SERVER_doPublish($hash, $shash, $tp, $val, $hash->{cflags} & 0x20);
  105. }
  106. return $ret;
  107. }
  108. sub
  109. MQTT2_SERVER_Attr(@)
  110. {
  111. my ($type, $devName, $attrName, @param) = @_;
  112. my $hash = $defs{$devName};
  113. if($type eq "set" && $attrName eq "SSL") {
  114. TcpServer_SetSSL($hash);
  115. }
  116. return undef;
  117. }
  118. sub
  119. MQTT2_SERVER_Set($@)
  120. {
  121. my ($hash, @a) = @_;
  122. my %sets = ( publish=>1 );
  123. shift(@a);
  124. return "Unknown argument ?, choose one of ".join(" ", keys %sets)
  125. if(!$a[0] || !$sets{$a[0]});
  126. if($a[0] eq "publish") {
  127. shift(@a);
  128. my $retain;
  129. if(@a>2 && $a[0] eq "-r") {
  130. $retain = 1;
  131. shift(@a);
  132. }
  133. return "Usage: publish -r topic [value]" if(@a < 1);
  134. my $tp = shift(@a);
  135. my $val = join(" ", @a);
  136. MQTT2_SERVER_doPublish($hash->{CL}, $hash, $tp, $val, $retain);
  137. }
  138. }
  139. sub
  140. MQTT2_SERVER_State()
  141. {
  142. my ($hash, $ts, $name, $val) = @_;
  143. if($name eq "RETAIN") {
  144. my $now = gettimeofday;
  145. my $ret = json2nameValue($val);
  146. for my $k (keys %{$ret}) {
  147. my %h = ( ts=>$now, val=>$ret->{$k} );
  148. $hash->{retain}{$k} = \%h;
  149. }
  150. }
  151. return undef;
  152. }
  153. my %cptype = (
  154. 0 => "RESERVED_0",
  155. 1 => "CONNECT",
  156. 2 => "CONNACK",
  157. 3 => "PUBLISH",
  158. 4 => "PUBACK",
  159. 5 => "PUBREC",
  160. 6 => "PUBREL",
  161. 7 => "PUBCOMP",
  162. 8 => "SUBSCRIBE",
  163. 9 => "SUBACK",
  164. 10 => "UNSUBSCRIBE",
  165. 11 => "UNSUBACK",
  166. 12 => "PINGREQ",
  167. 13 => "PINGRESP",
  168. 14 => "DISCONNECT",
  169. 15 => "RESERVED_15",
  170. );
  171. #####################################
  172. sub
  173. MQTT2_SERVER_Read($@)
  174. {
  175. my ($hash, $reread) = @_;
  176. if($hash->{SERVERSOCKET}) { # Accept and create a child
  177. my $nhash = TcpServer_Accept($hash, "MQTT2_SERVER");
  178. return if(!$nhash);
  179. $nhash->{CD}->blocking(0);
  180. readingsSingleUpdate($hash, "nrclients",
  181. ReadingsVal($hash->{NAME}, "nrclients", 0)+1, 1);
  182. return;
  183. }
  184. my $sname = $hash->{SNAME};
  185. my $cname = $hash->{NAME};
  186. my $c = $hash->{CD};
  187. if(!$reread) {
  188. my $buf;
  189. my $ret = sysread($c, $buf, 1024);
  190. if(!defined($ret) && $! == EWOULDBLOCK ){
  191. $hash->{wantWrite} = 1
  192. if(TcpServer_WantWrite($hash));
  193. return;
  194. } elsif(!$ret) {
  195. CommandDelete(undef, $cname);
  196. Log3 $sname, 4, "Connection closed for $cname: ".
  197. (defined($ret) ? 'EOF' : $!);
  198. return;
  199. }
  200. $hash->{BUF} .= $buf;
  201. if($hash->{SSL} && $c->can('pending')) {
  202. while($c->pending()) {
  203. sysread($c, $buf, 1024);
  204. $hash->{BUF} .= $buf;
  205. }
  206. }
  207. }
  208. my ($tlen, $off) = MQTT2_SERVER_getRemainingLength($hash);
  209. if($tlen < 0) {
  210. Log3 $sname, 1, "Bogus data from $cname, closing connection";
  211. CommandDelete(undef, $cname);
  212. }
  213. return if(length($hash->{BUF}) < $tlen+$off);
  214. my $fb = substr($hash->{BUF}, 0, 1);
  215. my $pl = substr($hash->{BUF}, $off, $tlen); # payload
  216. $hash->{BUF} = substr($hash->{BUF}, $tlen+$off);
  217. my $cp = ord(substr($fb,0,1)) >> 4;
  218. my $cpt = $cptype{$cp};
  219. $hash->{lastMsgTime} = gettimeofday();
  220. # Lowlevel debugging
  221. if(AttrVal($sname, "verbose", 1) >= 5) {
  222. my $pltxt = $pl;
  223. $pltxt =~ s/([^ -~])/"(".ord($1).")"/ge;
  224. Log3 $sname, 5, "$cpt: $pltxt";
  225. }
  226. if(!defined($hash->{cid}) && $cpt ne "CONNECT") {
  227. Log3 $sname, 2, "$cname $cpt before CONNECT, disconnecting";
  228. CommandDelete(undef, $cname);
  229. return MQTT2_SERVER_Read($hash, 1);
  230. }
  231. ####################################
  232. if($cpt eq "CONNECT") {
  233. ($hash->{protoTxt}, $off) = MQTT2_SERVER_getStr($pl, 0); # V3:MQIsdb V4:MQTT
  234. $hash->{protoNum} = unpack('C*', substr($pl,$off++,1)); # 3 or 4
  235. $hash->{cflags} = unpack('C*', substr($pl,$off++,1));
  236. $hash->{keepalive} = unpack('n', substr($pl, $off, 2)); $off += 2;
  237. ($hash->{cid}, $off) = MQTT2_SERVER_getStr($pl, $off);
  238. if(!($hash->{cflags} & 0x02)) {
  239. Log3 $sname, 2, "$cname wants unclean session, disconnecting";
  240. return MQTT2_SERVER_terminate($hash, pack("C*", 0x20, 2, 0, 1));
  241. }
  242. my $desc = "keepAlive:$hash->{keepalive}";
  243. if($hash->{cflags} & 0x04) { # Last Will & Testament
  244. my ($wt, $wm);
  245. ($wt, $off) = MQTT2_SERVER_getStr($pl, $off);
  246. ($wm, $off) = MQTT2_SERVER_getStr($pl, $off);
  247. $hash->{lwt} = "$wt:$wm";
  248. $desc .= " LWT:$wt:$wm";
  249. }
  250. my ($pwd, $usr) = ("","");
  251. if($hash->{cflags} & 0x80) {
  252. ($usr,$off) = MQTT2_SERVER_getStr($pl,$off);
  253. $hash->{usr} = $usr;
  254. $desc .= " usr:$hash->{usr}";
  255. }
  256. if($hash->{cflags} & 0x40) {
  257. ($pwd, $off) = MQTT2_SERVER_getStr($pl,$off);
  258. }
  259. my $ret = Authenticate($hash, "basicAuth:".encode_base64("$usr:$pwd"));
  260. return MQTT2_SERVER_terminate($hash, pack("C*", 0x20, 2, 0, 4)) if($ret==2);
  261. $hash->{subscriptions} = {};
  262. $defs{$sname}{clients}{$cname} = 1;
  263. Log3 $sname, 4, "$cname $hash->{cid} $cpt V:$hash->{protoNum} $desc";
  264. addToWritebuffer($hash, pack("C*", 0x20, 2, 0, 0)); # CONNACK, no error
  265. ####################################
  266. } elsif($cpt eq "PUBLISH") {
  267. my $cf = ord(substr($fb,0,1)) & 0xf;
  268. my $qos = ($cf & 0x06) >> 1;
  269. my ($tp, $val, $pid);
  270. ($tp, $off) = MQTT2_SERVER_getStr($pl, 0);
  271. if($qos) {
  272. $pid = unpack('n', substr($pl, $off, 2));
  273. $off += 2;
  274. }
  275. $val = substr($pl, $off);
  276. Log3 $sname, 4, "$cname $hash->{cid} $cpt $tp:$val";
  277. addToWritebuffer($hash, pack("CCnC*", 0x40, 2, $pid)) if($qos); # PUBACK
  278. MQTT2_SERVER_doPublish($hash, $defs{$sname}, $tp, $val, $cf & 0x01);
  279. ####################################
  280. } elsif($cpt eq "PUBACK") { # ignore it
  281. ####################################
  282. } elsif($cpt eq "SUBSCRIBE") {
  283. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  284. my $pid = unpack('n', substr($pl, 0, 2));
  285. my ($subscr, @ret);
  286. $off = 2;
  287. while($off < $tlen) {
  288. ($subscr, $off) = MQTT2_SERVER_getStr($pl, $off);
  289. my $qos = unpack("C*", substr($pl, $off++, 1));
  290. $hash->{subscriptions}{$subscr} = $hash->{lastMsgTime};
  291. Log3 $sname, 4, " topic:$subscr qos:$qos";
  292. push @ret, ($qos > 1 ? 1 : 0); # max qos supported is 1
  293. }
  294. addToWritebuffer($hash, pack("CCnC*", 0x90, 3, $pid, @ret)); # SUBACK
  295. if(!$hash->{answerScheduled}) {
  296. $hash->{answerScheduled} = 1;
  297. InternalTimer($hash->{lastMsgTime}+1, sub(){
  298. delete($hash->{answerScheduled});
  299. my $r = $defs{$sname}{retain};
  300. foreach my $tp (sort { $r->{$a}{ts} <=> $r->{$b}{ts} } keys %{$r}) {
  301. MQTT2_SERVER_sendto($defs{$sname}, $hash, $tp, $r->{$tp}{val});
  302. }
  303. }, undef, 0);
  304. }
  305. ####################################
  306. } elsif($cpt eq "UNSUBSCRIBE") {
  307. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  308. my $pid = unpack('n', substr($pl, 0, 2));
  309. my ($subscr, @ret);
  310. $off = 2;
  311. while($off < $tlen) {
  312. ($subscr, $off) = MQTT2_SERVER_getStr($pl, $off);
  313. delete $hash->{subscriptions}{$subscr};
  314. Log3 $sname, 4, " topic:$subscr";
  315. }
  316. addToWritebuffer($hash, pack("CCn", 0xb0, 2, $pid)); # UNSUBACK
  317. ####################################
  318. } elsif($cpt eq "PINGREQ") {
  319. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  320. addToWritebuffer($hash, pack("C*", 0xd0, 0)); # pingresp
  321. ####################################
  322. } elsif($cpt eq "DISCONNECT") {
  323. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  324. CommandDelete(undef, $cname);
  325. ####################################
  326. } else {
  327. Log 1, "M2: Unhandled packet $cpt, disconneting $cname";
  328. CommandDelete(undef, $cname);
  329. }
  330. return MQTT2_SERVER_Read($hash, 1);
  331. }
  332. ######################################
  333. # Call sendto for all clients + Dispatch + dotrigger if rawEvents is set
  334. # tgt is the "accept" server, src is the connection generating the data
  335. sub
  336. MQTT2_SERVER_doPublish($$$$;$)
  337. {
  338. my ($src, $tgt, $tp, $val, $retain) = @_;
  339. $val = "" if(!defined($val));
  340. $src = $tgt if(!defined($src));
  341. if($retain) {
  342. my $now = gettimeofday();
  343. my %h = ( ts=>$now, val=>$val );
  344. $tgt->{retain}{$tp} = \%h;
  345. # Save it
  346. my %nots = map { $_ => $tgt->{retain}{$_}{val} } keys %{$tgt->{retain}};
  347. setReadingsVal($tgt, "RETAIN", toJSON(\%nots), FmtDateTime(gettimeofday()));
  348. }
  349. foreach my $clName (keys %{$tgt->{clients}}) {
  350. MQTT2_SERVER_sendto($tgt, $defs{$clName}, $tp, $val)
  351. if($src->{NAME} ne $clName);
  352. }
  353. if(defined($src->{cid})) { # "real" MQTT client
  354. my $cid = $src->{cid};
  355. $cid =~ s,[^a-z0-9._],_,gi;
  356. my $ac = AttrVal($tgt->{NAME}, "autocreate", undef) ? "autocreate:":"";
  357. Dispatch($tgt, "$ac$cid:$tp:$val", undef, !$ac);
  358. my $re = AttrVal($tgt->{NAME}, "rawEvents", undef);
  359. DoTrigger($tgt->{NAME}, "$tp:$val") if($re && $tp =~ m/$re/);
  360. }
  361. }
  362. ######################################
  363. # send topic to client if its subscription matches the topic
  364. sub
  365. MQTT2_SERVER_sendto($$$$)
  366. {
  367. my ($shash, $hash, $topic, $val) = @_;
  368. return if(IsDisabled($hash->{NAME}));
  369. $val = "" if(!defined($val));
  370. foreach my $s (keys %{$hash->{subscriptions}}) {
  371. my $re = $s;
  372. $re =~ s,/?#,\\b.*,g;
  373. $re =~ s,\+,\\b[^/]+\\b,g;
  374. if($topic =~ m/^$re$/) {
  375. Log3 $shash, 5, "$hash->{NAME} $hash->{cid} => $topic:$val";
  376. addToWritebuffer($hash,
  377. pack("C",0x30).
  378. MQTT2_SERVER_calcRemainingLength(2+length($topic)+length($val)).
  379. pack("n", length($topic)).
  380. $topic.$val);
  381. }
  382. }
  383. }
  384. sub
  385. MQTT2_SERVER_terminate($$)
  386. {
  387. my ($hash,$msg) = @_;
  388. addToWritebuffer( $hash, $msg, sub{ CommandDelete(undef, $hash->{NAME}); });
  389. }
  390. sub
  391. MQTT2_SERVER_Write($$$)
  392. {
  393. my ($hash,$topic,$msg) = @_;
  394. my $retain;
  395. if($topic =~ m/^(.*):r$/) {
  396. $topic = $1;
  397. $retain = 1;
  398. }
  399. MQTT2_SERVER_doPublish($hash, $hash, $topic, $msg, $retain);
  400. }
  401. sub
  402. MQTT2_SERVER_calcRemainingLength($)
  403. {
  404. my ($l) = @_;
  405. my @r;
  406. while($l > 0) {
  407. my $eb = $l % 128;
  408. $l = int($l/128);
  409. $eb += 128 if($l);
  410. push(@r, $eb);
  411. }
  412. return pack("C*", @r);
  413. }
  414. sub
  415. MQTT2_SERVER_getRemainingLength($)
  416. {
  417. my ($hash) = @_;
  418. return (2,2) if(length($hash->{BUF}) < 2);
  419. my $ret = 0;
  420. my $mul = 1;
  421. for(my $off = 1; $off <= 4; $off++) {
  422. my $b = ord(substr($hash->{BUF},$off,1));
  423. $ret += ($b & 0x7f)*$mul;
  424. return ($ret, $off+1) if(($b & 0x80) == 0);
  425. $mul *= 128;
  426. }
  427. return -1;
  428. }
  429. sub
  430. MQTT2_SERVER_getStr($$)
  431. {
  432. my ($in, $off) = @_;
  433. my $l = unpack("n", substr($in, $off, 2));
  434. return (substr($in, $off+2, $l), $off+2+$l);
  435. }
  436. 1;
  437. =pod
  438. =item helper
  439. =item summary Standalone MQTT message broker
  440. =item summary_DE Standalone MQTT message broker
  441. =begin html
  442. <a name="MQTT2_SERVER"></a>
  443. <h3>MQTT2_SERVER</h3>
  444. <ul>
  445. MQTT2_SERVER is a builtin/cleanroom implementation of an MQTT server using no
  446. external libraries. It serves as an IODev to MQTT2_DEVICES, but may be used
  447. as a replacement for standalone servers like mosquitto (with less features
  448. and performance). It is intended to simplify connecting MQTT devices to FHEM.
  449. <br> <br>
  450. <a name="MQTT2_SERVERdefine"></a>
  451. <b>Define</b>
  452. <ul>
  453. <code>define &lt;name&gt; MQTT2_SERVER &lt;tcp-portnr&gt; [global|IP]</code>
  454. <br><br>
  455. Enable the server on port &lt;tcp-portnr&gt;. If global is specified,
  456. then requests from all interfaces (not only localhost / 127.0.0.1) are
  457. serviced. If IP is specified, then MQTT2_SERVER will only listen on this
  458. IP.<br>
  459. To enable listening on IPV6 see the comments <a href="#telnet">here</a>.
  460. <br>
  461. Notes:<br>
  462. <ul>
  463. <li>to set user/password use an allowed instance and its basicAuth
  464. feature (set/attr)</li>
  465. <li>the retain flag is not propagated by publish</li>
  466. <li>only QOS 0 and 1 is implemented</li>
  467. </ul>
  468. </ul>
  469. <br>
  470. <a name="MQTT2_SERVERset"></a>
  471. <b>Set</b>
  472. <ul>
  473. <li>publish -r topic value<br>
  474. publish a message, -r denotes setting the retain flag.
  475. </li>
  476. </ul>
  477. <br>
  478. <a name="MQTT2_SERVERget"></a>
  479. <b>Get</b>
  480. <ul>N/A</ul><br>
  481. <a name="MQTT2_SERVERattr"></a>
  482. <b>Attributes</b>
  483. <ul>
  484. <li><a href="#disable">disable</a><br>
  485. <a href="#disabledForIntervals">disabledForIntervals</a><br>
  486. disable distribution of messages. The server itself will accept and store
  487. messages, but not forward them.
  488. </li><br>
  489. <a name="rawEvents"></a>
  490. <li>rawEvents &lt;topic-regexp&gt;<br>
  491. Send all messages as events attributed to this MQTT2_SERVER instance.
  492. Should only be used, if there is no MQTT2_DEVICE to process the topic.
  493. </li><br>
  494. <a name="keepaliveFactor"></a>
  495. <li>keepaliveFactor<br>
  496. the oasis spec requires a disconnect, if after 1.5 times the client
  497. supplied keepalive no data or PINGREQ is sent. With this attribute you
  498. can modify this factor, 0 disables the check.
  499. Notes:
  500. <ul>
  501. <li>dont complain if you set this attribute to less or equal to 1.</li>
  502. <li>MQTT2_SERVER checks the keepalive only every 10 second.</li>
  503. </ul>
  504. </li>
  505. <a name="SSL"></a>
  506. <li>SSL<br>
  507. Enable SSL (i.e. TLS)
  508. </li><br>
  509. <li>sslVersion<br>
  510. See the global attribute sslVersion.
  511. </li><br>
  512. <li>sslCertPrefix<br>
  513. Set the prefix for the SSL certificate, default is certs/server-, see
  514. also the SSL attribute.
  515. </li><br>
  516. <a name="autocreate"></a>
  517. <li>autocreate<br>
  518. If set, MQTT2_DEVICES will be automatically created upon receiving an
  519. unknown message.
  520. </li><br>
  521. </ul>
  522. </ul>
  523. =end html
  524. =cut