00_MQTT2_SERVER.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. ##############################################
  2. # $Id: 00_MQTT2_SERVER.pm 17539 2018-10-15 18:47:53Z 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. # my $pltxt = $pl;
  222. # $pltxt =~ s/([^ -~])/"(".ord($1).")"/ge;
  223. # Log3 $sname, 5, "$pltxt";
  224. if(!defined($hash->{cid}) && $cpt ne "CONNECT") {
  225. Log3 $sname, 2, "$cname $cpt before CONNECT, disconnecting";
  226. CommandDelete(undef, $cname);
  227. return MQTT2_SERVER_Read($hash, 1);
  228. }
  229. ####################################
  230. if($cpt eq "CONNECT") {
  231. ($hash->{protoTxt}, $off) = MQTT2_SERVER_getStr($pl, 0); # V3:MQIsdb V4:MQTT
  232. $hash->{protoNum} = unpack('C*', substr($pl,$off++,1)); # 3 or 4
  233. $hash->{cflags} = unpack('C*', substr($pl,$off++,1));
  234. $hash->{keepalive} = unpack('n', substr($pl, $off, 2)); $off += 2;
  235. ($hash->{cid}, $off) = MQTT2_SERVER_getStr($pl, $off);
  236. if(!($hash->{cflags} & 0x02)) {
  237. Log3 $sname, 2, "$cname wants unclean session, disconnecting";
  238. return MQTT2_SERVER_terminate($hash, pack("C*", 0x20, 2, 0, 1));
  239. }
  240. my $desc = "keepAlive:$hash->{keepalive}";
  241. if($hash->{cflags} & 0x04) { # Last Will & Testament
  242. my ($wt, $wm);
  243. ($wt, $off) = MQTT2_SERVER_getStr($pl, $off);
  244. ($wm, $off) = MQTT2_SERVER_getStr($pl, $off);
  245. $hash->{lwt} = "$wt:$wm";
  246. $desc .= " LWT:$wt:$wm";
  247. }
  248. my ($pwd, $usr) = ("","");
  249. if($hash->{cflags} & 0x80) {
  250. ($usr,$off) = MQTT2_SERVER_getStr($pl,$off);
  251. $hash->{usr} = $usr;
  252. $desc .= " usr:$hash->{usr}";
  253. }
  254. if($hash->{cflags} & 0x40) {
  255. ($pwd, $off) = MQTT2_SERVER_getStr($pl,$off);
  256. }
  257. my $ret = Authenticate($hash, "basicAuth:".encode_base64("$usr:$pwd"));
  258. return MQTT2_SERVER_terminate($hash, pack("C*", 0x20, 2, 0, 4)) if($ret==2);
  259. $hash->{subscriptions} = {};
  260. $defs{$sname}{clients}{$cname} = 1;
  261. Log3 $sname, 4, "$cname $hash->{cid} $cpt V:$hash->{protoNum} $desc";
  262. addToWritebuffer($hash, pack("C*", 0x20, 2, 0, 0)); # CONNACK, no error
  263. ####################################
  264. } elsif($cpt eq "PUBLISH") {
  265. my $cf = ord(substr($fb,0,1)) & 0xf;
  266. my $qos = ($cf & 0x06) >> 1;
  267. my ($tp, $val, $pid);
  268. ($tp, $off) = MQTT2_SERVER_getStr($pl, 0);
  269. if($qos) {
  270. $pid = unpack('n', substr($pl, $off, 2));
  271. $off += 2;
  272. }
  273. $val = substr($pl, $off);
  274. Log3 $sname, 4, "$cname $hash->{cid} $cpt $tp:$val";
  275. addToWritebuffer($hash, pack("CCnC*", 0x40, 2, $pid)) if($qos); # PUBACK
  276. MQTT2_SERVER_doPublish($hash, $defs{$sname}, $tp, $val, $cf & 0x01);
  277. ####################################
  278. } elsif($cpt eq "PUBACK") { # ignore it
  279. ####################################
  280. } elsif($cpt eq "SUBSCRIBE") {
  281. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  282. my $pid = unpack('n', substr($pl, 0, 2));
  283. my ($subscr, @ret);
  284. $off = 2;
  285. while($off < $tlen) {
  286. ($subscr, $off) = MQTT2_SERVER_getStr($pl, $off);
  287. my $qos = unpack("C*", substr($pl, $off++, 1));
  288. $hash->{subscriptions}{$subscr} = $hash->{lastMsgTime};
  289. Log3 $sname, 4, " topic:$subscr qos:$qos";
  290. push @ret, ($qos > 1 ? 1 : 0); # max qos supported is 1
  291. }
  292. addToWritebuffer($hash, pack("CCnC*", 0x90, 3, $pid, @ret)); # SUBACK
  293. if(!$hash->{answerScheduled}) {
  294. $hash->{answerScheduled} = 1;
  295. InternalTimer($hash->{lastMsgTime}+1, sub(){
  296. delete($hash->{answerScheduled});
  297. my $r = $defs{$sname}{retain};
  298. foreach my $tp (sort { $r->{$a}{ts} <=> $r->{$b}{ts} } keys %{$r}) {
  299. MQTT2_SERVER_sendto($defs{$sname}, $hash, $tp, $r->{$tp}{val});
  300. }
  301. }, undef, 0);
  302. }
  303. ####################################
  304. } elsif($cpt eq "UNSUBSCRIBE") {
  305. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  306. my $pid = unpack('n', substr($pl, 0, 2));
  307. my ($subscr, @ret);
  308. $off = 2;
  309. while($off < $tlen) {
  310. ($subscr, $off) = MQTT2_SERVER_getStr($pl, $off);
  311. delete $hash->{subscriptions}{$subscr};
  312. Log3 $sname, 4, " topic:$subscr";
  313. }
  314. addToWritebuffer($hash, pack("CCn", 0xb0, 2, $pid)); # UNSUBACK
  315. ####################################
  316. } elsif($cpt eq "PINGREQ") {
  317. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  318. addToWritebuffer($hash, pack("C*", 0xd0, 0)); # pingresp
  319. ####################################
  320. } elsif($cpt eq "DISCONNECT") {
  321. Log3 $sname, 4, "$cname $hash->{cid} $cpt";
  322. CommandDelete(undef, $cname);
  323. ####################################
  324. } else {
  325. Log 1, "M2: Unhandled packet $cpt, disconneting $cname";
  326. CommandDelete(undef, $cname);
  327. }
  328. return MQTT2_SERVER_Read($hash, 1);
  329. }
  330. ######################################
  331. # Call sendto for all clients + Dispatch + dotrigger if rawEvents is set
  332. # tgt is the "accept" server, src is the connection generating the data
  333. sub
  334. MQTT2_SERVER_doPublish($$$$;$)
  335. {
  336. my ($src, $tgt, $tp, $val, $retain) = @_;
  337. $val = "" if(!defined($val));
  338. $src = $tgt if(!defined($src));
  339. if($retain) {
  340. my $now = gettimeofday();
  341. my %h = ( ts=>$now, val=>$val );
  342. $tgt->{retain}{$tp} = \%h;
  343. # Save it
  344. my %nots = map { $_ => $tgt->{retain}{$_}{val} } keys %{$tgt->{retain}};
  345. setReadingsVal($tgt, "RETAIN", toJSON(\%nots), FmtDateTime(gettimeofday()));
  346. }
  347. foreach my $clName (keys %{$tgt->{clients}}) {
  348. MQTT2_SERVER_sendto($tgt, $defs{$clName}, $tp, $val)
  349. if($src->{NAME} ne $clName);
  350. }
  351. if(defined($src->{cid})) { # "real" MQTT client
  352. my $cid = $src->{cid};
  353. $cid =~ s,[^a-z0-9._],_,gi;
  354. my $ac = AttrVal($tgt->{NAME}, "autocreate", undef) ? "autocreate:":"";
  355. Dispatch($tgt, "$ac$cid:$tp:$val", undef, !$ac);
  356. my $re = AttrVal($tgt->{NAME}, "rawEvents", undef);
  357. DoTrigger($tgt->{NAME}, "$tp:$val") if($re && $tp =~ m/$re/);
  358. }
  359. }
  360. ######################################
  361. # send topic to client if its subscription matches the topic
  362. sub
  363. MQTT2_SERVER_sendto($$$$)
  364. {
  365. my ($shash, $hash, $topic, $val) = @_;
  366. return if(IsDisabled($hash->{NAME}));
  367. $val = "" if(!defined($val));
  368. foreach my $s (keys %{$hash->{subscriptions}}) {
  369. my $re = $s;
  370. $re =~ s,/?#,\\b.*,g;
  371. $re =~ s,\+,\\b[^/]+\\b,g;
  372. if($topic =~ m/^$re$/) {
  373. Log3 $shash, 5, "$hash->{NAME} $hash->{cid} => $topic:$val";
  374. addToWritebuffer($hash,
  375. pack("C",0x30).
  376. MQTT2_SERVER_calcRemainingLength(2+length($topic)+length($val)).
  377. pack("n", length($topic)).
  378. $topic.$val);
  379. }
  380. }
  381. }
  382. sub
  383. MQTT2_SERVER_terminate($$)
  384. {
  385. my ($hash,$msg) = @_;
  386. addToWritebuffer( $hash, $msg, sub{ CommandDelete(undef, $hash->{NAME}); });
  387. }
  388. sub
  389. MQTT2_SERVER_Write($$$)
  390. {
  391. my ($hash,$topic,$msg) = @_;
  392. my $retain;
  393. if($topic =~ m/^(.*):r$/) {
  394. $topic = $1;
  395. $retain = 1;
  396. }
  397. MQTT2_SERVER_doPublish($hash, $hash, $topic, $msg, $retain);
  398. }
  399. sub
  400. MQTT2_SERVER_calcRemainingLength($)
  401. {
  402. my ($l) = @_;
  403. my @r;
  404. while($l > 0) {
  405. my $eb = $l % 128;
  406. $l = int($l/128);
  407. $eb += 128 if($l);
  408. push(@r, $eb);
  409. }
  410. return pack("C*", @r);
  411. }
  412. sub
  413. MQTT2_SERVER_getRemainingLength($)
  414. {
  415. my ($hash) = @_;
  416. return (2,2) if(length($hash->{BUF}) < 2);
  417. my $ret = 0;
  418. my $mul = 1;
  419. for(my $off = 1; $off <= 4; $off++) {
  420. my $b = ord(substr($hash->{BUF},$off,1));
  421. $ret += ($b & 0x7f)*$mul;
  422. return ($ret, $off+1) if(($b & 0x80) == 0);
  423. $mul *= 128;
  424. }
  425. return -1;
  426. }
  427. sub
  428. MQTT2_SERVER_getStr($$)
  429. {
  430. my ($in, $off) = @_;
  431. my $l = unpack("n", substr($in, $off, 2));
  432. return (substr($in, $off+2, $l), $off+2+$l);
  433. }
  434. 1;
  435. =pod
  436. =item helper
  437. =item summary Standalone MQTT message broker
  438. =item summary_DE Standalone MQTT message broker
  439. =begin html
  440. <a name="MQTT2_SERVER"></a>
  441. <h3>MQTT2_SERVER</h3>
  442. <ul>
  443. MQTT2_SERVER is a builtin/cleanroom implementation of an MQTT server using no
  444. external libraries. It serves as an IODev to MQTT2_DEVICES, but may be used
  445. as a replacement for standalone servers like mosquitto (with less features
  446. and performance). It is intended to simplify connecting MQTT devices to FHEM.
  447. <br> <br>
  448. <a name="MQTT2_SERVERdefine"></a>
  449. <b>Define</b>
  450. <ul>
  451. <code>define &lt;name&gt; MQTT2_SERVER &lt;tcp-portnr&gt; [global|IP]</code>
  452. <br><br>
  453. Enable the server on port &lt;tcp-portnr&gt;. If global is specified,
  454. then requests from all interfaces (not only localhost / 127.0.0.1) are
  455. serviced. If IP is specified, then MQTT2_SERVER will only listen on this
  456. IP.<br>
  457. To enable listening on IPV6 see the comments <a href="#telnet">here</a>.
  458. <br>
  459. Notes:<br>
  460. <ul>
  461. <li>to set user/password use an allowed instance and its basicAuth
  462. feature (set/attr)</li>
  463. <li>the retain flag is not propagated by publish</li>
  464. <li>only QOS 0 and 1 is implemented</li>
  465. </ul>
  466. </ul>
  467. <br>
  468. <a name="MQTT2_SERVERset"></a>
  469. <b>Set</b>
  470. <ul>
  471. <li>publish -r topic value<br>
  472. publish a message, -r denotes setting the retain flag.
  473. </li>
  474. </ul>
  475. <br>
  476. <a name="MQTT2_SERVERget"></a>
  477. <b>Get</b>
  478. <ul>N/A</ul><br>
  479. <a name="MQTT2_SERVERattr"></a>
  480. <b>Attributes</b>
  481. <ul>
  482. <li><a href="#disable">disable</a><br>
  483. <a href="#disabledForIntervals">disabledForIntervals</a><br>
  484. disable distribution of messages. The server itself will accept and store
  485. messages, but not forward them.
  486. </li><br>
  487. <a name="rawEvents"></a>
  488. <li>rawEvents &lt;topic-regexp&gt;<br>
  489. Send all messages as events attributed to this MQTT2_SERVER instance.
  490. Should only be used, if there is no MQTT2_DEVICE to process the topic.
  491. </li><br>
  492. <a name="keepaliveFactor"></a>
  493. <li>keepaliveFactor<br>
  494. the oasis spec requires a disconnect, if after 1.5 times the client
  495. supplied keepalive no data or PINGREQ is sent. With this attribute you
  496. can modify this factor, 0 disables the check.
  497. Notes:
  498. <ul>
  499. <li>dont complain if you set this attribute to less or equal to 1.</li>
  500. <li>MQTT2_SERVER checks the keepalive only every 10 second.</li>
  501. </ul>
  502. </li>
  503. <a name="SSL"></a>
  504. <li>SSL<br>
  505. Enable SSL (i.e. TLS)
  506. </li><br>
  507. <li>sslVersion<br>
  508. See the global attribute sslVersion.
  509. </li><br>
  510. <li>sslCertPrefix<br>
  511. Set the prefix for the SSL certificate, default is certs/server-, see
  512. also the SSL attribute.
  513. </li><br>
  514. <a name="autocreate"></a>
  515. <li>autocreate<br>
  516. If set, MQTT2_DEVICES will be automatically created upon receiving an
  517. unknown message.
  518. </li><br>
  519. </ul>
  520. </ul>
  521. =end html
  522. =cut