From 0d67041751cc7d4c4f8810f5e6862ca7df3d45ec Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 3 Feb 2021 21:48:54 -0800 Subject: [PATCH] Added Firebase two-way push notification relay. --- agents/MeshCentralRouter.exe | Bin 3064640 -> 3064640 bytes agents/meshcore.js | 7 ++ firebase.js | 196 +++++++++++++++++++++++++++++------ meshagent.js | 42 ++++---- webserver.js | 14 ++- 5 files changed, 203 insertions(+), 56 deletions(-) diff --git a/agents/MeshCentralRouter.exe b/agents/MeshCentralRouter.exe index 435d4f6c3d931747a130a29d7a5f3fcfeab63219..d85dd484132030609ed5864dd90e2f6b594b13f9 100644 GIT binary patch delta 25662 zcmd742Yggj(?5Rhxx05an?ico-Go$<5PC}j1QG%yAwjY!v=q`wLLfyHLUCC-B1#0g z(!@}t3n&^QQX)+dQIu+fh@yz15tJh0Z)WZi0zAd%eV_O9|HJ2gGv7IL=CnC;&)v3v z_HJQ6Tnp!lVvM0?zPE6=W@ATjXI96W&x3^Viv%H9-8-l~Gs-am>UN7p2*JYfMHa#A zx1mjYhX3N>;!MrD#c{U(X@LpAZ8?)}%<$uL{J2o($N4IQkSsTNbN7PY9P9AOM>tc7 z<$}m`jD1yU&uq%wL3X_-x~QB~?b-tO0A95#i&ZE^<%b#5g)9T8+8oF#LOh|k4GVL* zZ`ne*#D{khyyVXC$5HL1ZkNTp7rerl5aeEUT(0!tqa7tDsa=0&cu3n`c5BTC3zy~m z*1U^a!?=@1Ue=n&dI(1BvMX~6VwJ7I4P!qF1`U^Qw&vx+M|Gop`H!M-x-Kn{UlfEt zul+ZTJ>JFS`Zv0Kq6n26J!0TOJn7hd}q%2lG6^t45zKENAwCSzbAX(FvcQ(HXieewch}OFUbnHN%yIBtGi{#_ob<8~QZkCRnBV5%T zCA@VfzIT^f}b!-i&B$iCB-O|wY z$L{q}&Mb3uNst2@8iMkBekDOVR!?2b26SLoGmm$d*scuh#QUV*mWO^8CHAV&v7vqM zW;wIj9wk8%^Dv>$-;6jn7Yh)d0-ri|u?)ImW)q;A12g4l(;b+fr#2m8J`y!|TH~Zm z1eDm7A=ruM;!A>@*SAy6vKINalW7I3b+?CFY>Vewj$~XEy2&xuO1IAJl(Li*kt2k-E4|D9zBZ zmE28t=!mEVh6e++E!!>(8cKbs4;9{KgP}qfo2L2ITpv- zuiqce65G#vRXDJng}ZVc*e{W1D;?O^kthpQ7(N}0r5O%vO<%;rqs~@J>yP$-y091sZI}ZG<`ctW9N3ZMUAYqL>4Qr!B5nkf8v7^IM>()RGB@~3 z%oU>7ne9l$RiBiJ?$SySCmFEeb})es>{R5CXo*$8JUOtZeSDK8wg9GDVuzA`l_$67 z-X3?eQkejuFR?9wIB90a6vM+x7DaF*C^4~#aqAz0Tw12GF~IkWRBTu^^n(5GTiKLTtHuphwN0k#!TVzc5F zm>~k*;W+wx5R3=dIx6r_L`_>?#Ml(X2LE46C1&5ZKFXbmspDcGDfGnVUoXefH-i_b zFR&+}?gcivC%W!XiQRe~;#kMBa}hTHO3YM@6S`p(Z5`O5u-6v!!{VWYvuKnVgu7|G zo_YdH<7s?703BH65X?h&C<8@6I{1KjQm2PhVkBLN#SZOELtNSwbJ#nM8~mNw+zM>+ zuaVe^FFddla^5k2Nz0y3!{TSw0zqI^5z8E2@tQP`M;A=ED+- z7DW5As>;J!iPh8U?vKJ9bo~j8c~^JDC-X7#?fT(PanD0dhTcqDVkF|heuzP|NhVd` z<}rmMx_BWbz}AA)LECKR5ZozmLTE_rRY*!Y_A*4E1Dg)n(18u7{Zbo(oAw#nHBad< z8&;8xTPW&7`r+1{l(kuI?8M#7ogg{leQdA_5f2D>53}o2mJ-#>48+`-GnHM5!m{?P z4Y5Qji)3AiEhiSsqKVBVmcRxQ%O*C2Jwfc96!bF8%!V?H;K<@K%TjXLI<|xgZ&DNM z*fL_{x?9Q$^QSbuKAr$NQso>(@B+fv!P)chdc zir98y$=r+B9cq3!4?@klGTu$GEJU9Zc?>sWgP|cvU*fqOCs;dl*~nb> z3NPl27G~MV68C!5GA>+7B~k>o};i@E<)MwLm2ZDYWW&siBBkOJ+T}s+svVT zw%^LOaqbKB#9+nrO)_0zEvzHfNv`S2Lt7cr8CwaykB~B0KH8NV%;U-OfK};dWgl=6 zx^@y0V=gkF$ch}#ySHY7f>$Zh+C=j9Le3rVQHv{`e>3h zDwOjExC2%a){fJIJdpHD<9$HB-t~_8zux>MYDQ^Z>59_4(iMMd{y|q@hbVn{eDq0z zKlHJ4Hyp>)-GTvklWS%4%1kJ8vm~e;ls3_vDSni?$}B1~>Pxet*l!x!8SK3g&1?^m zWR8s~mB)`mxn35fG(S!!*D@Cj&`KdldAb;nJb)Ol8_;H$11LsF`c{Y%V!RRh77+tK6ECuUN{LFHqejRCfl{y+5PMAP3PMxBapClZ1z$|s>DjPPz#nVV?_y1jF`bN`q0;)IL&4=tm3S$HzZf)8M)olH0`#^s4-0| zS7S8%Y8VPO^9Jn%yp>iDi*hn}FQB?$FKGJoZ2(+1`mnqs5)K`=v(b2HJmTcE55R0h z-U+}ZgU$f1%*W-w=Z^Rl$)8JTQ>j7RQgq(ak5zX45^zQcc1&_cKf5Xs6C4qpa&cvY z?Hd92I5$GA#v%-+VPg<4nn&ZGx(Y;<8&2(mw&) zmukG6*kP!9KWK&*VBLJX`_0_f52RJTJjl@t7J=l?2f2Z~CSp6-*Y_}ExF|FK_?#EJt!@q&Qm~f+dlI(p zj`AHUR-*69h7H`0wjiFSKh%j@J~ z0?E6|uq*q>zKSf3(ecn}Us~PalvtG%oK8#Jljg6h8xAZG5aR!NB`r{6Tgi1d+HCzO zpyrUgoY0n>?x2XNApB0i?EV#HdQZ}%Qms(J71W_ARO@FdJxN$cN04)i4}|-4Q)L&vI6j&0Xy_(&~zX8^OOp6 zfKwsjT#`db4lfRq(_?r?GZrYhM#(bWU8uGfVq_lr{vfv+yrz$`Kx?^n^8l+Ho(Bwd zSOj=R^D^K|?kiC?u7+D@aH6fV6ALFCnZF+NT`D#MCfRNWT#)@1;B$lv(z9`o+K+w* zRDOg<_&$(-7x#IYrmdrFmEnsno&uDtCsLe{`>SPBf?d2v>$E-o!Ai7CLA@v8I!c)j z)&@>^$p^v1@|1i}h_p$Cc($yh1p_dzd5nZUIGr1?ArV^;z zqfUbC6?q2mGTbF0y~A-EUZ~2#37a>nlYF}$JijVcPepY;(uHchCwG-3W1Fni8#M2W(6~8Lg-jJbu^Ax zS!hqXXHFS4{R!xuw(>eu)_C6nyi;)t@mW=4v1~G{6kQ>T_;sVU@Co&&NVlm>6 zu87~I1~hl2zd8goBM93QULgFH@EG9zPG5Cu50>475F0_3*nD4{iSN>IUAGkB^WdWk z1a~&&;qdRT&7=A2{yLqZa}KSRbr!EWUGu^YAvS`1zoQMQUG$QpKc4t{I*N4g^P>{PXL?$=d~zTUZY39I!uzEc6*bF~ zHF4ZKG)m9eD}H3j&bsykwt<+1{b*CE71^#pEVHo7^58h0>-1ES1g}7OcBBgAdS9H? zwG-Q}sKNDbaXhS56hzANotj)Wx9`(nvKcEsSuIxuZCG7H069a(yRWH3H?egeFV)~P(2bz(;qr8ZVRfXZho zqgfX=VgOlcm&<=7@NgPK1XIE5LCW+b%dvxatnidPXAln+(&Zh4_<&y6Mgm(k^uEzh zeFFQ0n1xLZt<(-;Uszd1oWzn?^a!-Hu&)C%<<5z`dl))MW#44ocTk{DWy$br6|~7L zW1<X)amGPx#^$EBC)GnwxgRKaW~I>ukl-idr3%DL&R#vf4VI6>NV;gD|cE$9Deg_MDQDLp2-kU6B5waQ<7x<3; zEjE#ug$?MtPu}+g_o8^&#U`yMBhNYVcTez0`@{OZtYQPIFc{ho;o)YS{6j2qGb(35 zm?9glFqYBvxc(5^U}f>}*5P6H1+m#|amc6o!|cWutXJbXCvAxBQO33+Tf(Mx{Y(!G z{!L>wzG7LU{+N|5@%?$sRsD%(=4veaMgOst*7zDcZs||4Vse8${A&nbI_#tDX!ZsB z<#n{0t^Fq34cJ{PYa1WUzGSZ3l(Oh}H(mEukjcJgixoAr=wagFts7I}#TNG6JLHwAN57)I%zh_8LJZef#U8Y@u+Pdn8Gd2Wdlh!i5MlV0-QA~n zG4zNq++Yj$-!F@nubOzOIo|LG+i*ZpzC3W5{w~{TWvAkn>F=@qR>mP{nD7^|*{rGi zGCdb;4=VNEFjZ<*f+w-r%(a(|LG%+w^@S6MG#Nr&8&DXWKCOB03T zzuYe^kQ0YOK3XO}HIxs4%sE^L`CV~{8E~YKd|P3V0rgqJt5z1)xl)@aINnjpFew%Y zH=7iOIlVyeylX8B%B*WOjKdpf^W~u7(9&{w$Z#Gh?33pWhs3!_ZWzwJ(y`BLgpT$C zRD#KZEVD*vv7-!Ygx*RSJM0+E)(ZQrEGq?`xP;ygRF5SS8&Ax_I>{YJa6|h6Y0>O8 zVPPx9#Kmo^;WZQ)rk*mlw+tJET}l}=yGNcd0y?!%-Zp{{fEoL31l&Xq$iqhRVDJ1A zco{@^w+mLbu*L9>@=@W{u8LQ^ynG}y)f4u~F=2ae#iVT}vQx1NYuBMtdra6gK&en1 zUn^e%uL*xZz@8M&B`8KqfPE_14N}+#z)lO%R`v$4v%)khI|h?{URY^mGacjQ)C}I> z^bHvOCODduR;uDZkb^RLu;>wQk%wgR`EjQV&V2kZ#eF37&y`;ssW9HYQtQgU&r;YK zxS_c7oNR^J!wtog->|ab0h?sMEdHkWamr`%*(@F_&dix7duQ`tx>LvSn8jF4S;iQi zNle*rF?=yG*l==rHk>MOrDOTdm#6|Ol-FnTsT2$;{FgPTT*4A6$2p|%z_kjCsEkpk z@nT|13+a5V!dM|3AJTdBI>nh?F|t#%*t6M8xq1{V>67xlQ9P7d7{#Z&`JjbSe7lul z3%UG;mHnt~zGbN7Ij0q6vcoMy6@S^vez5<`@Ff4(%DBT{hVfi` zMzLI_k&I9AXe)aY*wcKXm5p?_H%{Q&t*iprME<>%tuwkBtGVx4#mn19*A(L`3fsb z1~!kMBDRF#+C0k(E-OBrixZ8{@$ug)%(aaptKkQUSuiq;>_u+;K`E>mBmo;w%)-9R zO_S&6aRag}8lae$G%o@;+cqZ;-~-zqr^)Zr728~LQ0s9eG>cbR6~%vTaqVtFgyr7&$HJYj6* zpIO;Y^0s^)o{s0^o&0xFTG)4R>)XkbZ=o|~d%ex)5>xJHb-dyaEK^+9@wp1q9tGER ze2bNxmU|WO@LuS8FaLs+itD}b>AMH6_w(n7DXts%&cE)vo-1!HfZNN9^3ehw8;6Pf z6MjYopAoP&{0aX>VcOjEXehH+Q<-)`x*ITGE5m1wPkEG;#mcFL*2jzU{Fdgvv!(L# zLT(6MW4y>`YEfxnON`%U(r1 zn2xnS^9{~aPdi1s-}p1XXk`n4{lYz6l(OG7M~qE8*~&E9BgS9(Tq}FY?S%0<-*06b zfZgD?tjyi+wDC9I$yM@<-4s(XRN4e7lH`8a6K} zPc4YaepuPUs*{?GDsisA!loqNF>1xK0Ax$pt8MCyHsWF{gYg-4;=5M%RNxfRRtyYO z>ZKVs%iBi7=iByIQHi|pkQ=VdSFqKhO(2KtkQ+6hNC>H)r zV@rI8MA~~lU<>*hynUL>qCBqZ1De^<)Ije*(HNq%;GI_$0yHxeX$@`SAhG#K54$E< zyh>%t(?+m(i`XLVhNRBk!D4het7?OY z%rQ)}^RxEce<>>}g6?ak$q;jmh(8~Pe+EPesiSh@f zd_b$o#aMQ$vtk(}w;sd2%~*M27ey6VfNTS?CE7O%1_9egOvytb;`_vuaDoL=>ap-P z$zW;hie4Ipje0%2_tG*d!YG_XN-Cp;`|Pl=$6bH?X$+j*(q%&#{Yh2+%rE2K zhIH?K*6N3a*WK5<4S3*8KA_nB0e@Pjr;Qepn=SXa8x1MVY?phEY#PhGhN01jX7wKT zPxTqiY?phPcXl&--M!j7-^y_GqlJRzvOVtb`Ilz4%YB;M0M4QNpO59?-sx!R%12YxKRYh{=NPm14I8Ro!G#H9X;7s!FePsPju$Y!&`d`EUxe7S5OQOb7b z9t;nb>psIUJM8^givgYSZfs^aeLp-@?`m_|9`_%;e`;pC+<%kzRnj7yucWxV-CTxV z?mqA$J5|Bts^pGUWS&rE4bN68=sFV_cIkP&Q#0G;?kKMUTMAE~X7wKTS$f}Qw#(h$ zC!m>Oiy>B~glA}T*&g@KK4HylmwTk#>PhP=cWGAdaWB(%Yi7IL4KGw!r zia&l(^nW9V1)+!QT@fAJ{wrO^YBar zyI^EsC<$!63E{nOH8 z8~O1-cw`pM|8~Q^^w`l)xry2QSGr~X|1@7Pp$|i{Ok7?tDliPoB<0?bjBgsxlH=sE~I|=KD2X50S8vBqq~E^USNSFtm6 zJi7!(GS2>@M=qQ^&Xi-@PX*Z3fv~3c@q7645S|X@^qA)o2QOORm)ZAPiMOY znn(3Y)ZkNs$mUV_DGB0%M#UD<@$)%~md6`bR^p)nbfOU`H;hN!DbY;FXls+ufZRBX zdx!0TFE*;!o3JAA0WFAZ&R&335ZOL>E>p1(*dl7?Pg{MG96B3rg_H8oR8@|q9jZ`1 z2O7?{(GH3TMHw{$Q#Q(fxzw(uYxyMh=>MX%n2|V+)Sftw2eQa;e5!}XCy$-OyW2n1 z33=Nb?w$US>R-+g?nG{VAow@)MMI8l>G~j?5jgt+P+%?Pf9J?DmwP|b?aiPHYw7#n z`M5q8a@|7}xv<;4aQLW@S+ZyD?8wKBd#!UXfD)6*Cuhdrz7)IPfm4%H_h>V3X?dY?O5!Dyy$s& z?WAy!8%*O2-B_WXt&4vS`cgD-u@IwCgF-zp5%86G=phT*7%|45w7y_7q~7ATIMn8?%8&{n(YFsdew8l2gKs~RTSJY946{}8pUQB1;0taNH+<$i=#!8 z;FlIB9u?j!Duho(VeFd(>~52)O72?&FLHgG@w9kbJq+H9X;S@BIbCd^_q3W+!~JK0 z=FPxo0cYne6dR!#M^y0GgLJyTp=$Bqc4-;nl-6b z$m$p2UiNc7lG$>{7hwof@;0dIg=+soScEo}+f@m|yrS)(c?MERy?}|PN#zE+yGfM> zgQ{j2!HsI17n881{EEs;!r*9Bm-M`+s%96*bXJE-?e&d3RQk#@MqRC5YBNyXK*O(5 zt+&Zj*9#wHm8v^SB|R(Dks)UqiXLnPkmc$5o**;EFGM90Vu6ChrGzf)uHB;Y~t^{tl8CY;sr0Bqj~glMLhMFb>^pFLOS#Lgx@8g{ApAs%ca;TA*>{<7Seme z>R5=(BU}oYA=Hw*k+6m|CK2~~k9wOFc@8svsW5cVb@)xw;tsggx9>1If| z!dTDez-Sm$s8|2twg}`4@yjKP@JF5-G?wVO8su#y8vz3#m05)RG25kD2$a!+MVM}1 z2l|p$V}*^RF^O!@Uhwd~X|J@ys`;(+A<*3LMZDbB57HeKG)V0Oj!Rw==ENGo9QmnK zsajK%C)Eh6p^G&_sXS*f_m0JY9KkUWjo>(_1`5JPaN)?VNC)C>NLQqS%9~P?G(oV_ zn8Y!8PMRxHQh^Y@GsEpfTbwaOd&_rJY5cbiF@a!r=8K% z@+7wlnk!HYLBQ`sey_RB_XKCM+Z=^Nq3ErO?cJXeJ6b@D$iCW3)AVk9(yyLGwmI4H%tC zl|ddHR0Eini+Csj@u^6}usFnTWnl@d;Pq4_W1(7tCXaA&IJ%f`d_`*`UQOGn9ihR& z+K3e%nNls!^LR(wna0tX#?i$3BpriKq_p?>i13uwOWZ%^TfqFRliW!(Bwp3}iM?F! z0=ABT6fbTnKzznG6Ht^_F5w=za#DcJOxoBp*`V=SZ`dG>b{oM75gu?O^=2FWRr5(iQBI>?XqxV4zcz_)Xt)HYG47yoLTybit<5 zD%-_fwyCzt`8hw?n8drFF^SI(#8`Vj?vON-7Wp~C#e^#eEgBVkRKTL~2Q-Pex)7W0 z(QKY3s?1(z(VXgor5(*MWGIFYlA6TD`H1u3qfs#;24w=FNsQ~_0u`RK8!J?*PIb+L zC=M>qlbVFf(Nk0=F=ce5&PH7TIkcK>^`5AkLgSmlwv<1kt7R*CZh(8on}N^iLe(D` zYIG(kVa#-~RuidRsT26R?q=M?PmBSl5^myJ=Hra_^coYP?^rkw>~=!D=?yzjQ&_QD zw^4IBI)a^-ei*$$w^Vd-b7Ykq@AwwzZZO~_Et~}4w*Qu{gx`Xj(>!rs(fhg@C_MqV z-{UmY+FAaEu3qzFxY2BD60<|#EU1ya0&I_JdPaxZ){1edaLyC!2>ZKavP!x`S2Hou z4bpYasE)SPYVR>IYLj?z3>+#oi{iV1=8cL-+a}e|eWPu$`8eD2usw#@HVOrmsem6; z;5`U$Be^0T-f)j**@|A$3OQ;i4;`HanYvLJ0iUvK6ov;!uv$%wc9U%&%_eM0xFH0x zYZ*y)YvEv+Yqte%OHbS3{TPnOkme@aMbfRvj&8NJbaJT|7C<{&=%&>GiEoabN!%xY zxD>KH=He#t8MrMri5K!Bn2C-F&@gn>B=}~h*vDu-G7PhCVjmf@?VB`se=~`v;W#8{ z%Z&5w;nd;1**;gq1F?Q>+``-T4bsIi+~K5Vi;Xkj8NH{23C^YP zyE&SxdLM^sb+~!VI0wA3Om+x`yL$w?LYPbORzg?_m?2bKHF&~oRK>x!aF#L+yd3PO zT~y)5Dpe=7CmbL}xYTHC1+({Ahg=a8b&0e$KTj%=PD5B(gmxL2(!Y;ETtP|xYF-?Y zte~$POyV1z#|n|!AF|4&O1djllEx$&oZQ$+3CDdB?xn3-9i)lvO=p;;LQ+ITs}gCe z9J-u)XY|Z?p;fgyF5{(EPMQGyCff$;wMqM}^UhW#?Wm$%fRQ14TaC~@8+Z^bLt*s2 zX_Mw^KQWAS>dmTpc2gZB%|VI~JJJNQzvSBGuv)ko1F86^c+EJAw2t+N;CTLG3Ax;tC6ZV^Uj69n97!ArHrHwbKA1)FN^a5Qkg3C^LaJ|)pCROM{Wdj`lb{Y=2Rk|VP2#@t02>UNZ(N(i1Z{v#6SuhBbj1vG z4?L|HU1SSK4_8~a5geajYE<}0dS1#jVS?RR{*>ENG2eHpn~jZa)eN^QqIcCix04j( zCq>Wv7eThqU+;DvG_M0*EPU53SGpMR70B|9S-LeOWfsa1)QR-0!-6Q035EH0+^|@0OpWfpqmYHv2H$KnQkFq z1?k6=ey(nU8C1`aY9Xl>Q{|U+FM)oAZW-XKx>bN{b!!1P=r#gw(Y+41UH2y7PTelR zUAjGh@9L;?y7xdns5=7qzV0~SaotJ4k9B7NPw74fJg2(|_=WCU53`niLk)jVu7A}1 z00qBL^EasSE#1$cVYWCvl`W1>Yl~ybAx!~kib+!enzn2*3-)cxX0Wcl9hfb9-lqd| zVKShBy$u+^_5p^mvw&R@&8!#u5=bl+B(iHDr&4hSVLp>ucVJ^!r`ChW^&oIv;9E*^ zDabGSK1=emAn*5`C%(yid^ZvvP_1DzS|3ntW>Y*4sOAG+vf>(cu(ha0O|+XC|KzwK zP9uDk(Bg#}i`ODHsdX*MwTf&&eXRlY4J0>^yx$x37YUgU%D#lG7s-SdJnF{>Kk>}SMyPVXyegE zPG8H130>r^YxyYiFnFght~>)Uyes0WO2iYsh&Q}O0Y-JH7+!Ql>*8qq7lu-h@tt2A>?jtI5ym)0P$SG z+`7is_yo~OgnQ3GVX*M?82md#aFt6o!RI!Pros<~Ps3OEMds;a8PDinSqi?0{or4` z$7hho9@X82H#HyC)ki(5Gr=!8KdKw)F*uuaFze0#OddxnN!hGn%;XVX^tkRYbq4kL zg_rrm7>oEEe;13fw?U5HCz9+6^2Nz&lJkedn;tq1$u~gm^s<&@^C2L^+vteA2=cU@ zwj?hf!C1~QJCgf=qv9vwXOy7<=b?=Gj#uPJkk47=aP;HeiflaLZ93n(@RAwWUk5+? zzs8?mW$Y=CwJTjnE&`tm=X(|r`5GJWhpz#HQ4u)J*=QhnF~}E7j3mDYl|SC?Px4@t z3j#?F0(qBH8;N6|Js(W^pXA9~_~p7$Tlt+>o6&{iMo@I~9eu7me9pyZ=MKNT zWOq#BiI{Z**5^4sJ7GuFh^fZYZc!H(TwwQB)D4JeeWC5aTR%HH?R)XFSx4SnI4Cvy zz1+DhcKxUeyVso#Gq;+!X8ru}=Xamqx~|t}PkDIk?{WL=xk=+QUB$4QQx6$K9p1jJ zY7B8&w&N?iryXKFOIvpsH2(ZH_3m-1XA+l=J||~>Z~uy8;pAVIEz<~_E2{M0?)4d_ zdvVu??oW7JpVT^YNaewEc5|xR4DK(iGR@RS2)C!E?u?szU3D?b_{N5+*ZH{q^0j>( zZkW4u>eQ`k_bwwl^_6Ve&W@jb(_XMqb?&0-)On=O%w6Nh`x}=B^)6aD;@4sCHzv8y znEY;^&tn!3X;c4o{RbZhN8j1AaM`r_x4pAp;hpT89SDM&sit%GZ0PTKM>k#H5u6;~ z@lcp|-h9u^TP*yoSVzWh{sUB}8wZ!T$jB|ohFRLxJH z?XOB)*Od9}vmf4E_FI(83(-6GHcSaE9g#Jo@y-itKB~C<-ZjJdFH`k1Qs42*+_h}g zRli@R&av+{NHnL#M19-&*|CWeV!cxRj}GZ`(6MO#+>bu?h)Nmu(aS$1Z9cuXuua9& zBee%_c01SYg^rn*+&>&V{FP-5QNdS#>UF92fU3y1PRzXY*?!?u@6W4N+}`w}_NvEv zHoMi)vKt;vU%l@ky>l}!bKJ5Q`)2QZ|Kz=%GiGlKn0;r9_S0`Se>o?}JkEUU2gjFx z{qcFXm0PC9y03j(HL&4g+Rvxge*e13wS?uK9hg%7it>0j}{$Hc=xTP6?b+m`1G33ye)Yj|1l}UepXk#yK&OxU5hkj zzl|RmaOwMA!P)oTZy)zzd13FiPfU5c*GnDO3gvmKofGHu8RTp;)>QSy^_$PU{NnoU z>*TCE9iRDabuVqs%ZtAn-)`0?S$SH&y26aJb;FLm*=pIq8I!c7Z})!VYNw3CQ@elQ lCd2Le#NU38o!Yi*$J_4yr@OH~`ra~r;u!Dnyy_Zb{|}p7c)Rwfs?^+4)L4|ZbU;d5DJ@!U|7)#trJ=9y^}Y9dpU(;jycU^B+cQHr^d{GcWls5yznL(-xP(JZuh7cl5ez8_C z`wa^YXZSAxF3yzfL4VHHjTe{z+?q4h>S=y_rXLp?{5W5!7hI*iUfeBA;Aq1qz0a9K ztPn(|W$d%Aw#=%+4dgM6*{m|u9i(8Xhd1vacu3{&$3fw!cp{s5&3&0MA;@jaacQGB zFLF>iDxc`f^p9x6rDPvINVqP&?87@LRg62Tq_aLeP6xdT2JG8~IR>$=K0q1UD(H_2 zQhQ%sA)IVj>C115!nua$1Nmh^_+1(m%)?cl;7f&0S{2MU3ZBxS5FQ}trK%9_B^ac6 zA$%~f&qH{gpl|3C%H4#hd(#*Tc!sfp>5Tb~0XcQ-n3B;&Phro~e_||f1nAOJ*}8Or zIe;!8K0UE7{B4x#!gw4sdNzz_IKG#zVm7{r8@zu^vuE9;-fehC^D0mjdm~2r>@u3 zSQm}>eJZwD46T{9>{U>ySTr@w)3E6iZabo#SYlC0kR2Nmit;|ck{~U6ja(cEXvfZH z9qpoGYckP^w@AM{58J637+R@iLwYx5JFyw=B|$2tFk+iO8gOoo6(Ej-Hnr?nIk;kG z1z={!dgcC@X2<+IeoTYljY7@OJ~(L;0afhd!RSPFLP?MlyP1X<+Z#~=7gcOLbGK5l zLEgBwy}W)*gXMq~wqv=ZuQKAC1$Rf>I`H#SCpO#_ZJ&t2A(`7}ryVQ8k7#YSSy?od<99AgkC!h+hd+IV#3eAW60726+y zc&{Qy02gC+M3=GteHc3@I1Zrzk>YTyefs?3q+)w`Xr&#iAGyY4$8JR(sIp^UM4|j$ z6^2hc!_Z7S_G)j$Z6gj;sn{jAX*qVxzo=)v9ebUu?#Hc>V#B#>)b`{xCKc=MjdR{V zJ|kAetRVXA*!8S+{wgLwv^ueuQ*o7xvd}!X1o3HoPibB_cQ@N6tJqwaNfmoD>GO0u zmhOdKUhqa7qVEgM)komO*uZ3@G64c!#Wn__=9>bXfFrPpRBRmP~RWw>}TJ=Z0~2A099;^G^HJPZ)-mg{c7Wj*dYaRum9(zDrVbz zN3q7k%1Xd}+;gQY_VpQRCG z60W07x%Wvd?Lxi>0NSyf!8k3~D9;4JrvH$6kf%MXFkU{7L+9$vY4~Sp1g5HY9M<_e zv2m5y?Vhn*C}plsU0gXJlaLat}1Z<%*H&xY{@N7`)4!+h?Y&GzIr90 zys0~6N)_`c+z}n1U`dR5v#y16WBl2OszVwW8OOQV6OB9P@{=sqj=8uY=H}xDu<3(4 zM9f1?rVerYaKrP{C~nqg^k_Cti&=0C)o~3&eP|!tvPIdi$JnuFT?WUf*hjtRD(#qw zHj5?h;stqf&DSgqi|I%j31nAjS&n9A-(XPBXI4SR{`=m;!)ovnmhsROekwwbM_ zGHoO@BWz_Gh*c81#BLDlK^r7V}bx9yikeEnO~){ACi&z z^B;*_9D%GeS8;UhYh-5Dms{}~Ff?_?!U5cx3g4yf2XK30TZkodPhvk)_rrJqF(Z`~ z^0vf2qeUy{orzr=iEU2gvBbhckuBmTj`RCt`pn@bX8kfR=8RTmrZgymd**)+hZ_^C z=k;8K!_}9A8S@kB`K!btpOo2JVuLK~bq>w5O%}G9*v>(6&23ckX-ioHF)!(G1P`;< zq%*bxntq>@E|Soh>+MZcbH7DtC$$BoS#J6uKiR73gZwP#7$!z^_aMJOtc!(Rq?*^s ziG%!8V&N8ch1diO`Jr`bBk zZA6?ASE_lAI}nSrFc)GHjq@D$AU5ApW+3)#CQi&b?n~^I5#5kpv`~SnerjRglBb{Y zK&raNQWi=qT$CaL?TGv4|MTuoP&ab-vMX}; zvMc`7{U5HF1t$CQ=g}t#e&A!r&Nz-$s9YAjZLFbZf*PmIdQKX&05!Ky;yS72gt=YQ@XU&l_$VC zm;2eSqoLHJzZv5^us`CSgr@*UcbNz{(G^$yRM#nhyBSt}gA7)dO$B*1$qPuHKyn5x zP$|jNNM6ZjLQR#3xVhwcz)k|nT}eNRVr6nar5RMGXtV7m+(reR$e}pGnWVpxPzN=C z?!FlCGFt}NrU%Nssk8&(ON5811t%(%Vn$-qa#b+b# zs9cyw)MgK3&3%PIZLFzKVl@1qPlr0=2W$iUI;{~FjQJ%9^}4gywo z!}|Gzfd-VX=Adm{0%CF65wPl?cMNdafU_PdGkY!{!{IkK#1BcnsKQEBYQ&|5=RN#b zRm3I0X`$$-$_X1>P~V?c{qz8PPgoR^ zKN#Qw@~e@Xq5cli$P4l3C5SQ8+Q>=tMynM2E6MVnHd5-4(@qIxivU2_LXn=(joh@H zErw8fmRENlh2dL-0`2wggg#v`w0|c3QNfHvPCj;2Ab0Z(70Ygofo-E=S5nSH&Ht{m zf6y1k|KpuKoVpn%FBsoHOHHsGl#X~7u5Rlz^^X(L!vSn*+3FDga&oAR^n=b8GTi-g za*(q^Fm0+p+Eh0Pd(d*o8A47e9<))1P|adOImt{-#7*0!CvMcTXv8#-RqS9_bk|)M zjNm~ zz9ke5T0seQHgQZQeF^38d`-Zbmj!IWkz*Y%LKhKhW^*hZ#j$iI>0czwqf%e9h{ZRl zcz}q;LkPnZXgriQ&q%uYo~Ir04Bg*G6Lz6{xsLD(9X;gWZJ<&+YC*nD%Lfw^wRVCA zlSIq(WM_*1#dKqkqy1-ElE8_nbfel!H=+Im@g_8s&1>xR6@7&2agCrYN~hHEpTQZX#M8WElSkX)ass2s3Sohx-zzTE&h88zENl zFoO9%9ppoWn0iw@5#@kgNDcYXlqS;5Eu{IVrcs&*ZD{${k^VWtuW1p3X|`sPb8#;b zxeVUHt5G`KHbNxs&bng6y-L(%lRVW5HLczHH8}&EOO;N>aA)%$dvl&C8`Lk*?c&eg zW|FrrlRuNlAK7Pl<$IA=uTjdafE_KcbmLTb((=tktnQ6C!pa2tPvMxRV#eWx&{y8@ z3QQm8_2})dL5SlN=+JMJCT5ZBIubR3#ciayvAjL`D`yuuuXJ%HV^753JZ!tsG!Yu@ zGol81Gug}n9Ah^ZFwAZN;KynSaFN?`lnwQ8vve6~?c~S;3FGr$1HDh>2EZii&45qm zYzM3+d^$ZF6aUYJJ3wVug!n$+4fvC|+tXOHh7we!FE%j_P-Quj;)Fb0pPy(=ZV;|W zK&SUo((_C~*Oi0|X!JTtq?k70jF-G0T9}oR=K--*G!jpjRdhg|4G4!9+Yh9ZeYjum z$Pw7eo!*G71aZIV7+A`oxh47p$evMW0Z+qy5nf}012?<^m5mcNenfkzV;nq}$|k6M z7`1;s0Lb}iFFZ=Y>v}Mle^gUWv+`YCuJ0z`gPFWH)Rj1RIdor-N4Y8r@yj8GAn15{ z>V=l>*U)z9MYx?dPz-}%`3to$}6j++hWNGbmhm&AHZ1cbr>NtDZk7}}WF0oQsJogk%OV7r0lVe_?3SKtyX#Yn$cOSOn$hOF8 zXeT^@w+-tFVevwTmUe(GAy&(nwo2o~)Nc~~i3>OGKqI%dbK z;Pvd73{=;$F9I{Al7YNSTXZ^=eU<&NcD^o^CBsXB&;c7=7Q^7hx;&X>NQVdV`1CSe z7V|Ab6^wtN1IuCM7WN*n5v@z{0Mor|SxtOO;$!u9+*1 zOyXS}e=w<7C0jmTRx5)trPq^qH?O6-2`srY!L4&d?^Jy$t%p?8n^1!vxP6nh5iG! z>RxBF=E=<6Z>eq*tDG;hcEC2Xb#*fP80@yPzy&h10lRH1^hIQKEIx3D?kzTvSS{<< zd$;aw=CTmW4zTz2uB?G25vyf+b};vi?CG^+=rLCE8O)<>59)Ta%5|v102w)$ck{tD zI>@44N98ow0?3BRjAcfADD51~^`=w0L+s8*tWxJOHf^x=VR&g4*yB91V&6+;-{_9FFroZ=-3be;^VPfG)tzC*)C&62EtxMhznc@oF0os0V67RN zlR2)y9$1)rLJa$qxonop!V_G9`C3>)+5_EZEYiY;r7`_ytdGpt(rl&vb5>v}`!pnr zeZeMM*zfts7Rn5q7;NO-%*RvV)e3g)9qNevQ24UuD*J)h0XEm!Mt_anBUa0{_^H@+ z7Pu2@*0L*w&id;t;a%D8{R+MQ8#dpFKpd@S@~@LrMd@fyM^tIU#k0! z?XfTpk;R1HiOpcmU6$&&V0}QgTPf{Kf%C>tcuPnn%p&Cs_UmYPtwcCxVV_l~m|D0; zY%v>cd{VC#3J#+AVm92E2JC=^1$58UYlP54a+!U16R^n^_Qt?sy_Im;!j=vk4XpF~ za?SAx731_;VY`Kntauuj<^wFOWy<&(y|s{LVcEH}^tQru3%d>|vi7?eZRMc6}Zh9*e4Oz$F`w=l@Z5QB{cJ+u`JXVFCr+)vYZMOzHq4nplkQc~b{5Efb(25v`TtA&M26Vffo zwwvI6#nMXJD(Ss+$T6AH#dOG3*hj43-}JEbHOX%Xl|~P-lnxY*-g;P?FHIW)8G5O- zX$bG@ZZ~`$E4i}Q|%M4SY{ZM$n=LeJ2W+*oZnGNwnIlPHAQOX_$4a|~e z4&zb6ZfWl@-YsgqZk2Ey-*K>{mRAH{xZz`~=-sPAds}2M1CTLZ68W&TIN-bWghl&bB$qDn$<#qg>_1cVXq1EiOpcnt{e5QVKs)?rQCJ9 zex1-LmqEwxN}DryH*ok`2CQAC6h0j8iTk8^!+A)2atXX}A-vm3?xPmoc0DYtijcig zXT|{AL#&qdwh@3W?;+O=f$eZa*wRyG?$WQrxn4|9SS9H*c}Vzmh{2P>`9!&N8nDxX z%>bD-0y`_jSlG(kSyDwNELyxYCzJ1WJPD;g3JylBJA;i#I3i_d@eomwP$SLE;&W_I z>z(-6p>j*Xl210*`|O7sfg8`wk(ECuZq<44I~F!9V7-))&9{lWQZ7jLIXq6Rf)3Mj z;P67h!($gxPY|GUa=?o`X>AVN_;In~`1VDz7b()`Iebbw8l>=BucC4>>smG1E`= z!Y^6a>%|wO`dk?3E&XV|?hIAYgxTDZE2*!(>XkW)hD)FBe)@>I{>3rGVS*n8vqTn4zS*VFvH;HI~UaauzQjHbc|h zHqJ1MFSoE{+c>Erk9&&07Y&f+fMCWoU%(CD$n8Ee0NGe#wd{x~)v$n1ydk@Bs?%E6 z#oXbh%%-SJ28kbSVI_v8eAc&enZl+_w~R-ACo{}hE4bm7%phm!R`D&(G8-iYzt^jX0qlz!}t8Mh0O(ai+ecBWk0D88Jc;rg{d`%3_tK$ z7PiRsnBg|xV`1xn-Qo8v%+2PE;YZ%VMQ$Yw*iXFJ!aNYF(RJ}WwiHbFAmRGFuA6#H73b)hIZ7IV*`7_&O@hY+kJZ$X_1BHZN` z`wot>@%n?!?XC0jZYhg)|3(+k!ro2|@CpfjY3#ONK=T9(OP5MYdAc4|#U11dw@0J#d<(OaE|fxYij>4M-p{_c7|ZT;lxsRkL(8~V zJQ`2zB&!??kgX%OShKPq5!idgKBa{uL>T{Fe+}e zsCT*D@cOoeHM-sLy4AvPnSZn}h$hK#46SJUF*Lb}!1SWGpa4gh$PNiuU1?!hy><-m z206A!uy3id%WbaCv4u6d*?YUSFf{l2!yN3qTgrC1P1pIhutqmO$^I!y;6*}Mi+Yz^ z2k*8mtkEsfJG_Nq;~gzb-WZ))%67Sx={mQtMz{Ww`6=!f9S9%5;w;4!R$Zc#Nv6YUE8({|hJl0v_)L=a@5?Q8P?6PU(NJDgI4| zZk;Y#yY)!w z_>*nxntztBwn{q>(?_eFC9D2^{`hkT|00XDT7ni^^M4s{Ee-vL?U*0l?f#8y_>ac% zH@lZNb}OfKe|?*k22bJc&aLXgPR7lvkgBI}ucCqQ-WI%R-1YGQ{Zkiz-NRoD3cJL;p%_n#lLjI((j*B+rR4&o|ZV{S^rs}jYMO4y5yWHkMIyHDj+ZCiJ6 zw*U*CjX}h_H)k(KU~>`Pq>;8wS@c&9)sx%97Y6mrWl%Gs%PQXOcfge5m27yoBV@t-auoq1&Ar8Ig>ry_h338(0F zIJYQix*m>Fjtwm3R4PyAqou7^C6FFG9y_1CIGqO+<6xC+^WQkbm<{OBCI|=x+fV+U zfO8pVzr*^%cdVd=hiA7>C6k>T@6gl99Q%IVxqikZp-rD4x0}Viq;>Vklqx-QAB+nV# ztL=sy9Q`U-4}m>Ndw3qKfymzTM7=9)86~r)lu#s@z`$IThgF+Lqr>_!C?5n3XPaq9 zMTVh_8iC0h=l{6WrghVj)lBf{f6-gJ;W&=e?l_J=WRc|#2>bcBH(l=k)vjx`=~_>Gp!E84JV1?U4mK+hGZGEWs?}f4s=Tk^8^kc!AQKtW z9p?{?f?81iWee7n_YfajK{KVW)=sugc7MqKFveRgeCxFMhy1Us0+T2)v|ipnzi479 zm3*FuFT6hzG_CrVr%0}9dBsICfx#mo)q0q+fgEIi$p6}5oWa(2W|SYd3S>{RKWmt zMG;?~#p>Cd?jr%;$X?8v6nr3j!-NgaTMfUDVwcvy3~W{55zv%$|A+;W{t41|CVe33 zn-%@4zGI!)$E7i5c2am>uY!h*+B;wjD%O+C`jS}^nKdiCL+yklDs56MFUkT}GRg&T z^ooNk;7t=+-K+=}F2FDZa5~Djvrx^h7W4qbPreCC9GI0d*?pkU1cOWyX46oI*3qA( zg!P2$3AYmN0L&CTm7@x9R$hi*PH1Ac(oJBDO&i6X9uCY;S)A(1>R@7vKr^AuD0D*l z@CDwZIAwqKLN?l+d=Sa*OE&FqwZC)l2|8wGf$R2n^xyP3Z>9T7_S{i2TqL7>qR zIuZ&3`r@bTPJ>aQJMOcZ~^2S(9i9d zW}(R{6U;9;@`vvEkLM5vuM`?3DcGb9dpI6g8g* z#NiTFv%RqJ)vOlY{e=&Z!t!(k%oI8b%Fgf=Ss^WKl(3bYtY*6cd-GV~J41iotavIf ziPs8l(#rYV%k)O}BEDCNLAX`8nz@W`72foSWILdtSNIO0)_)z88nv5vqA+pz+x)N+ zOB017!*_waIP(CH5@-7#;d_PeLQYcCXThpZ+9yz|@{DBAndDW6St2_^yrM*}jpS(> z1zQ?Lvr)j%H40aYV??81mliL=M~MnY!iR=1n2iED*Q^*(kRygEPw7XB)eI-Hn&A}2 z3a2v1i}#g7;f1wIG(lsRv{cm+CkV1g=I@v11OPGzpg zg1nugl}a|ao)XE@C}qmkI&EQL|r<@SQ2Svac# zjZmh}f*)Nd$y=w`AyoU1gjI2_+M-Al#ush@O&O$*9l{N1#RBf`i8EysmR2+=JXJrq zxw0$DlJ37Ls@XB=7!<>Miw>+#vDPY2xkG5oE>(6^m2|IEMyW70RV%~1`zx{k3CcP} zxc6-3ePyjsr)*{kNlTQmDzR#{vRb)4^C-yv-lswK_r9QXb|`;weG%lN z3CmQq!u32?Sdf7puYkO~WIdn*WUE>sxonH79s;FEs1>H#Hh{jwew47DG)D2;#NE)s zTgKh0trpFX9S?%$jxXZXHhz%Ns9>)uJm9FxQ-x`-PB2HER#hqH7v`zzgn8g%oiJ2- zZz1=J!+^}-n0zug4r(t2;T34YfisiuYzW=e zCA_obvxtXz_DL&N3F-p>0`*o{icdKTnnPTfrk+oAchEp&=jk--xh}*{b3q6%2myU*U~+W{K*-gbTZ26LSnNYplfcY1=g!Y8n-fYo%`*wfjG#3KwRO<-356RheKiqIS=wGS!NJWq9=r#jD*W+7=7lBSug zn)&?@{H`n3UrwdV*$HWo1TWuXqek(o-si1KU`%+IJXiR+Rh31)-~XyrwM97Ouujh78xc zC`xOkG(rxoW}CbwYA4h9CbOl|;3bf%x|U59>(x=36>4ICfM;SDs!Yy0Ryq)kS z{frT@&UX}K{+*79cfDZ0sZ%On(XLltjfrFzROgD;X_t!5t`4kIxZMR2lq3uX75Wgm%@Y$fE;~A=ntQ6yCMt=iDdQa zSj`6OK$^8?H75Q*b}=)_W;NV@Og0*K_CJOF34seLX6J0%>N_GfO zL&qEG?z9&Y+)NvzxDif5Moe}#MrE4cI^n(=Pm)G)Vh4zXX0Z%zh|S{Byhx~o8}^FO z4@P4YY;#g^5;I4OMzP87YKpQkEO?SuHK6+AOBQ(uoooK3m=JZ1Cj zpwH}#;+q{u2~nDJ*%hiP%Jo&GF^YOeS9Vf`ck7dI%e1#YK-1Wh&MHfVq{vA764gtR zX&D4dNaj5IYGvolMfQ&B0Nn=bz2u`&^R?4@+LN}P zNpng++_5Jc)4j9e0BH_TfY^{G(9$J_Odw113l;)NZ$mW$2@i|knGGWxOEhhRM><;3 zGl(PLuZ~q3%n{X^T04bPGpnn#cJc(hJz&;IH>X;SYox)+2oYE$cq(@J20@*YiuH?!C7iB= zoDU(R!PDDG5iwRVZR7^$lcKWwDd#%HD`u~>I=gM!_I%b zpjLfS5$*yzhF0Q&dV5@&OOs|-?@X6w(llv4h%-U`MJ~lI&5A?1N|2xGKfz@$%gs)) zH7Q<;o8nTUSerKzo*Tmprn+2EY>A%@=-T@Qz`)*;?q#mEHCDagVic27m$+0Zo|Zbk z%mbXqm%>kqSS{5DIn^o-hQ8tAXw^q5dKo4RQ+2bru_C|<v{n+ZvY+}`L3%;bvfWOkl!+oT@ zH@7-Pa;Nry9pbyW9iUimRveFxq|z!kIDc0zcH03pmjmVoZUEewx5MouXy9OErE`AY z&18kL$qJv_n-#(F&(dSTMYsDb*VPh;@ z&mybmwUf+HFrQQl$#{u&A!wFsmjbTRz6`ip`zqi%?K;4X+D(94v~L1#*ERq)YTpHX zS4+-m_kw&tdkF9Y?T3IzwI={iXwL$k(S8DWUi&%VCGA&$UqQcS4ZBWF-_$mN>K1i> zhm7xOzXJ`k#_=hvaeNwU98)f73P@8-no7{LVUt*}ZyPp^b@FY;tl^g*+c9Uh7*NmN z0t|p()ojPwvX22H2z#(g-tEjRjtT~{Z=oQS3Ni`vnbM~nD`V|_22j%jpy_ zMzoD3IW`644n8I#CL}(!B-fI>Lio{rsCd`?9p!EJcj4Lx*CFNYhK{fD5rU|3?v znhzCxq#LXG2=fql^RIKoaKLU6h|gCc9`{AOBo zA*dG%5W|#(fB^+qS{;KLbpYa@ZKrt-H$%Z^#aMiZus9vfWQWdqm4Wuz)&-WG@f~=X-D<@Iih?u8B4#V(zApI0L_feCE`KGwuI#g zShcAaVpb_)C(^%=jtzb3jQF)b;;-(AFXbSfrw*<~VEv2~)Lf_1{p4Szcrw-qp#B{$;=$z4X~mnX-8I zPauyyu4{s~B_G%Ah<;p`48KAAxGuxJdk*Pf)?5B1yKk=|Ws8QfWcR?rKkE*WGpHBf znmClP$cyrCbiWF6%zGl~1&}XKQj(lJ4Bn~Gs!6^Aa)%`vlFe@d85XRS3CK0utx2Ah z!C3AQ8c$Id}-HwM7oZcHHc#juPH!^L+rXhk-8kpf_%BeK=NiVKC#oE0aJjxV+)=fy);zKe+ti3VfS$+;4 z%?c$rF6)J14J$O^Rf5!Es_o#Jw%YIpTW!0{d9KyT9`IEc__-JO3-83yKZWGj#>Kf| z{?c~C1a$}26@JX1W5f3^wOi4`q!*XBi)|^oxV&AsEuYv`@|$X#Lmm7>SQVhOUA(bf zRfjTVf4$E>A;tFO_(NV7Uh{Y9R5SU(qMzPgv+}j1KEpOS#qZwRcWp)Lo6Gn8;M3>m z0jGxgjqMgXb~t-;@oNKS^Cg>IUu|r7*6xVQoUgX^TXRCl8$5fKVSd1keG3L958d<8 zq$l*%x7*c>z95a<-EOwI;HA%Qyi?uJulI`!Y8Ec&IM`ZMd2HrWKfhRhICA%|+XrUH z2Vc9qepk2Di?4LjFIjfpD{1jNC*GQEI^1~M_Pfyf#hXft6Ac-^hE96>gWF$My(Wgb zUHsg5bJ{CqO&fOA4H^1X#?6@73+#>t?CSaS@8ed4|9W>!-=-dGt%h`XV`HpOtufgA z&Y2&*+8x-iqlaOTPxO*ZHgVIvx7PjU^uevXgwZoqB@Y};{lgtI+Psz8%+4v+bbtD= z$)@+E1r3{;{3}1PKcVhF)iKCvLC@m$PkQaV?UivXXNz!Fdg{`KvcAuhtxs@KNVWw} zc6rx4)Nb0q+1B$MYVN-|yK%{Ls`tySRu4Pw@0ZSh36#EjuiYJUM8~L3UAsgL@6cOi z)n-P*jJvjim7-%OMTbw^J5|~KIILSkL)@y(5nsi3y;dH&tLEcdm%raLZ<+T`UYBlP z<_Bjjv(I`JRL&Q**}DHz z|L>nT8hx$sYoYzIL!Y#beqnOVJw=Uu%7iNY?r$^)-Z;?hTWj}*9p{dnFn?zL?s(Ux zF`3a5?|57<>d+^l_DSm%ww$L{Q%Ftw(kTUI&$7a`|~RH&KlGE&HdhOnl?1Td8p2*UGT`KdR<@SHPXE1zRP^g z1pBL1O&@Ju;@$Yf?46!#`fuD8@_#QoxIh&ogrZ(dq+ zalyX9-M@MN%GiM65A?r2_2s&t@CU>DPk-}^N&Ny3el+VFm*d QYzQlAZg;cBWX0J31KS21+yDRo diff --git a/agents/meshcore.js b/agents/meshcore.js index 2c8df3b6..5bebf0b3 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -803,6 +803,13 @@ function handleServerCommand(data) { // Perform manual server TLS certificate checking based on the certificate hash given by the server. woptions.rejectUnauthorized = 0; woptions.checkServerIdentity = function checkServerIdentity(certs) { + /* + try { sendConsoleText("certs[0].digest: " + certs[0].digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("certs[0].fingerprint: " + certs[0].fingerprint); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-digest: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.digest); } catch (ex) { sendConsoleText(ex); } + try { sendConsoleText("control-fingerprint: " + require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint); } catch (ex) { sendConsoleText(ex); } + */ + // If the tunnel certificate matches the control channel certificate, accept the connection try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; } catch (ex) { } try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint) return; } catch (ex) { } diff --git a/firebase.js b/firebase.js index 8fddc412..72add8ad 100644 --- a/firebase.js +++ b/firebase.js @@ -18,6 +18,7 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { var obj = {}; obj.messageId = 0; + obj.relays = {}; obj.stats = { mode: "Real", sent: 0, @@ -36,17 +37,27 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { // Messages received from client (excluding receipts) xcs.on('message', function (messageId, from, data, category) { - //console.log('Firebase-Message', messageId, from, data, category); + parent.debug('email', 'Firebase-Message: ' + JSON.stringify(data)); - // Lookup node information from the cache - var ninfo = tokenToNodeMap[from]; - if (ninfo == null) { obj.stats.receivedNoRoute++; return; } - - if ((data != null) && (data.con != null) && (data.s != null)) { // Console command - obj.stats.received++; - parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid); + if (typeof data.r == 'string') { + // Lookup push relay server + parent.debug('email', 'Firebase-RelayRoute: ' + data.r); + const wsrelay = obj.relays[data.r]; + if (wsrelay != null) { + delete data.r; + try { wsrelay.send(JSON.stringify({ from: from, data: data, category: category })); } catch (ex) { } + } } else { - obj.stats.receivedBadArgs++; + // Lookup node information from the cache + var ninfo = tokenToNodeMap[from]; + if (ninfo == null) { obj.stats.receivedNoRoute++; return; } + + if ((data != null) && (data.con != null) && (data.s != null)) { // Console command + obj.stats.received++; + parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid); + } else { + obj.stats.receivedBadArgs++; + } } }); @@ -102,6 +113,61 @@ module.exports.CreateFirebase = function (parent, senderid, serverkey) { xcs.sendNoRetry(message, node.pmt, callback); } + // Setup a two way relay + obj.setupRelay = function (ws) { + // Select and set a relay identifier + ws.relayId = getRandomPassword(); + while (obj.relays[ws.relayId] != null) { ws.relayId = getRandomPassword(); } + obj.relays[ws.relayId] = ws; + + // On message, parse it + ws.on('message', function (msg) { + parent.debug('email', 'FBWS-Data(' + this.relayId + '): ' + msg); + if (typeof msg == 'string') { + + // Parse the incoming push request + var data = null; + try { data = JSON.parse(msg) } catch (ex) { return; } + if (typeof data != 'object') return; + if (typeof data.pmt != 'string') return; + if (typeof data.payload != 'object') return; + if (typeof data.payload.notification == 'object') { + if (typeof data.payload.notification.title != 'string') return; + if (typeof data.payload.notification.body != 'string') return; + } + if (typeof data.options != 'object') return; + if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) return; + if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) return; + if (typeof data.payload.data != 'object') { data.payload.data = {}; } + data.payload.data.r = ws.relayId; // Set the relay id. + + // Send the push notification + obj.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) { + if (err == null) { + try { wsrelay.send(JSON.stringify({ sent: true })); } catch (ex) { } + } else { + try { wsrelay.send(JSON.stringify({ sent: false })); } catch (ex) { } + } + }); + } + }); + + // If error, close the relay + ws.on('error', function (err) { + parent.debug('email', 'FBWS-Error(' + this.relayId + '): ' + err); + delete obj.relays[this.relayId]; + }); + + // Close the relay + ws.on('close', function () { + parent.debug('email', 'FBWS-Close(' + this.relayId + ')'); + delete obj.relays[this.relayId]; + }); + + } + + function getRandomPassword() { return Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } + return obj; }; @@ -118,40 +184,102 @@ module.exports.CreateFirebaseRelay = function (parent, url, key) { receivedNoRoute: 0, receivedBadArgs: 0 } - obj.pushOnly = true; + const WebSocket = require('ws'); const https = require('https'); const querystring = require('querystring'); const relayUrl = require('url').parse(url); - parent.debug('email', 'CreateFirebaseRelay-Setup'); - // Send an outbound push notification - obj.sendToDevice = function (node, payload, options, func) { - parent.debug('email', 'Firebase-sendToDevice'); - if ((node == null) || (typeof node.pmt != 'string')) return; + if (relayUrl.protocol == 'wss:') { + // Setup two-way push notification channel + obj.wsopen = false; + obj.tokenToNodeMap = {} // Token --> { nid: nodeid, mid: meshid } + obj.connectWebSocket = function () { + if (obj.wsclient != null) return; + obj.wsclient = new WebSocket(relayUrl.href + (key ? ('?key=' + key) : ''), { rejectUnauthorized: false }) + obj.wsclient.on('open', function () { obj.wsopen = true; }); + obj.wsclient.on('message', function (msg) { + parent.debug('email', 'FBWS-Data(' + msg.length + '): ' + msg); + var data = null; + try { data = JSON.parse(msg) } catch (ex) { } + if (typeof data != 'object') return; + if (typeof data.from != 'string') return; + if (typeof data.data != 'object') return; + if (typeof data.category != 'string') return; + processMessage(data.messageId, data.from, data.data, data.category); + }); + obj.wsclient.on('error', function (err) { + obj.wsclient = null; + obj.wsopen = false; + setTimeout(obj.connectWebSocket, 2000); + }); + obj.wsclient.on('close', function () { + obj.wsclient = null; + obj.wsopen = false; + setTimeout(obj.connectWebSocket, 2000); + }); + } - const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) }); + function processMessage(messageId, from, data, category) { + // Lookup node information from the cache + var ninfo = obj.tokenToNodeMap[from]; + if (ninfo == null) { obj.stats.receivedNoRoute++; return; } - // Send the message to the relay - const httpOptions = { - hostname: relayUrl.hostname, - port: relayUrl.port ? relayUrl.port : 443, - path: relayUrl.path + (key ? ('?key=' + key) : ''), - method: 'POST', - //rejectUnauthorized: false, // DEBUG - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': querydata.length + if ((data != null) && (data.con != null) && (data.s != null)) { // Console command + obj.stats.received++; + parent.webserver.routeAgentCommand({ action: 'msg', type: 'console', value: data.con, sessionid: data.s }, ninfo.did, ninfo.nid, ninfo.mid); + } else { + obj.stats.receivedBadArgs++; } } - const req = https.request(httpOptions, function (res) { - if (res.statusCode == 200) { obj.stats.sent++; } else { obj.stats.sendError++; } - if (func != null) { func(++obj.messageId, (res.statusCode == 200) ? null : 'error'); } - }); - parent.debug('email', 'Firebase-sending'); - req.on('error', function (error) { obj.stats.sent++; func(++obj.messageId, 'error'); }); - req.write(querydata); - req.end(); + + obj.sendToDevice = function (node, payload, options, func) { + parent.debug('email', 'Firebase-sendToDevice-webSocket'); + if ((node == null) || (typeof node.pmt != 'string')) { func(0, 'error'); return; } + + // Fill in our lookup table + if (node._id != null) { obj.tokenToNodeMap[node.pmt] = { nid: node._id, mid: node.meshid, did: node.domain } } + + // If the web socket is open, send now + if (obj.wsopen == true) { + try { obj.wsclient.send(JSON.stringify({ pmt: node.pmt, payload: payload, options: options })); } catch (ex) { func(0, 'error'); return; } + func(1); + } else { + // TODO: Buffer the push messages until TTL. + func(0, 'error'); + } + } + obj.connectWebSocket(); + } else if (relayUrl.protocol == 'https:') { + // Send an outbound push notification using an HTTPS POST + obj.pushOnly = true; + obj.sendToDevice = function (node, payload, options, func) { + parent.debug('email', 'Firebase-sendToDevice-httpPost'); + if ((node == null) || (typeof node.pmt != 'string')) return; + + const querydata = querystring.stringify({ 'msg': JSON.stringify({ pmt: node.pmt, payload: payload, options: options }) }); + + // Send the message to the relay + const httpOptions = { + hostname: relayUrl.hostname, + port: relayUrl.port ? relayUrl.port : 443, + path: relayUrl.path + (key ? ('?key=' + key) : ''), + method: 'POST', + //rejectUnauthorized: false, // DEBUG + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': querydata.length + } + } + const req = https.request(httpOptions, function (res) { + if (res.statusCode == 200) { obj.stats.sent++; } else { obj.stats.sendError++; } + if (func != null) { func(++obj.messageId, (res.statusCode == 200) ? null : 'error'); } + }); + parent.debug('email', 'Firebase-sending'); + req.on('error', function (error) { obj.stats.sent++; func(++obj.messageId, 'error'); }); + req.write(querydata); + req.end(); + } } return obj; diff --git a/meshagent.js b/meshagent.js index f5660bd3..374dcafc 100644 --- a/meshagent.js +++ b/meshagent.js @@ -1176,7 +1176,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Sent by the agent to update agent information ChangeAgentCoreInfo(command); - if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null)) { + if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) { // Agent update. The recovery core was loaded in the agent, send a command to update the agent parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task // If agent disconnection, complete and exit now. @@ -1489,31 +1489,33 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { break; } case 'agentupdate': { - var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task - // If agent disconnection, complete and exit now. - if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } + if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) { + var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task + // If agent disconnection, complete and exit now. + if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } - // Agent is requesting an agent update - obj.agentCoreUpdateTaskId = taskid; - const url = '*' + require('url').parse(obj.agentExeInfo.url).path; - var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid }; + // Agent is requesting an agent update + obj.agentCoreUpdateTaskId = taskid; + const url = '*' + require('url').parse(obj.agentExeInfo.url).path; + var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid }; - // Add the hash - if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; } + // Add the hash + if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; } - // Add server TLS cert hash - if (isIgnoreHashCheck() == false) { - const tlsCertHash = parent.webCertificateFullHashs[domain.id]; - if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); } + // Add server TLS cert hash + if (isIgnoreHashCheck() == false) { + const tlsCertHash = parent.webCertificateFullHashs[domain.id]; + if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); } + } + + // Send the agent update command + obj.send(JSON.stringify(cmd)); } + func.sessionid = command.sessionid; - // Send the agent update command - obj.send(JSON.stringify(cmd)); + // Agent update. The recovery core was loaded in the agent, send a command to update the agent + parent.parent.taskLimiter.launch(func, null, 1); } - func.sessionid = command.sessionid; - - // Agent update. The recovery core was loaded in the agent, send a command to update the agent - parent.parent.taskLimiter.launch(func, null, 1); break; } case 'agentupdatedownloaded': { diff --git a/webserver.js b/webserver.js index 71baa0a7..cf1bdb86 100644 --- a/webserver.js +++ b/webserver.js @@ -1782,7 +1782,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { parent.debug('email', 'handleFirebasePushOnlyRelayRequest'); if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; } if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; } - if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.firebase.pushrelayserver)) { res.sendStatus(404); return; } + if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; } var data = null; try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; } if (typeof data != 'object') { res.sendStatus(404); return; } @@ -1800,6 +1800,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } + // Called to handle two-way push notification relay request + function handleFirebaseRelayRequest(ws, req) { + parent.debug('email', 'handleFirebaseRelayRequest'); + if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; } + if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; } + if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; } + if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; } + obj.parent.firebase.setupRelay(ws); + } + // Called to process an agent invite request function handleAgentInviteRequest(req, res) { const domain = getDomain(req); @@ -5184,7 +5194,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Setup firebase push only server if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) { if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', handleFirebasePushOnlyRelayRequest); } - if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); /*obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest);*/ } + if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); } } // Setup auth strategies using passport if needed