From ec72b5b91a40bb7494687a1ce80ffc50f3714e65 Mon Sep 17 00:00:00 2001 From: "didier.amyot" Date: Fri, 19 Sep 2014 20:02:10 -0700 Subject: [PATCH] copied from a project that was version on mercurial --- client/.gitignore | 5 + client/Gruntfile.coffee | 48 +++++ client/bower.json | 22 ++ client/coffee/babApp.coffee | 37 ++++ client/coffee/controllers/aboutCtrl.coffee | 4 + client/coffee/controllers/homeCtrl.coffee | 8 + client/coffee/controllers/loginCtrl.coffee | 4 + client/coffee/controllers/topBarCtrl.coffee | 9 + client/coffee/directives/service.coffee | 31 +++ client/favicon.ico | Bin 0 -> 1406 bytes client/images/bin.png | Bin 0 -> 32160 bytes client/images/gopher.svg | 68 +++++++ client/images/logo.svg | 20 ++ client/index.html | 52 +++++ client/package.json | 11 + client/partials/about.html | 25 +++ client/partials/home.html | 10 + client/partials/login.html | 27 +++ main.go | 106 ++++++++++ main_test.go | 28 +++ readme.md | 3 + webapps.yml | 15 ++ webappserver.go | 95 +++++++++ webappserver_test.go | 213 ++++++++++++++++++++ 24 files changed, 841 insertions(+) create mode 100644 client/.gitignore create mode 100644 client/Gruntfile.coffee create mode 100644 client/bower.json create mode 100644 client/coffee/babApp.coffee create mode 100644 client/coffee/controllers/aboutCtrl.coffee create mode 100644 client/coffee/controllers/homeCtrl.coffee create mode 100644 client/coffee/controllers/loginCtrl.coffee create mode 100644 client/coffee/controllers/topBarCtrl.coffee create mode 100644 client/coffee/directives/service.coffee create mode 100644 client/favicon.ico create mode 100644 client/images/bin.png create mode 100644 client/images/gopher.svg create mode 100755 client/images/logo.svg create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/partials/about.html create mode 100644 client/partials/home.html create mode 100644 client/partials/login.html create mode 100644 main.go create mode 100644 main_test.go create mode 100644 readme.md create mode 100644 webapps.yml create mode 100644 webappserver.go create mode 100644 webappserver_test.go diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..9a30aeb --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,5 @@ +js/ +bower_components/ +node_modules/ +lib/ + diff --git a/client/Gruntfile.coffee b/client/Gruntfile.coffee new file mode 100644 index 0000000..478a3a2 --- /dev/null +++ b/client/Gruntfile.coffee @@ -0,0 +1,48 @@ +module.exports = (grunt) -> + + # Project configuration. + grunt.initConfig( + packagekg: grunt.file.readJSON('package.json') + bower: + install: {} + + connect: + server: + options: + port: 4506 + base: '.' + keepalive: true + + clean: ["js"] + + coffee: + compile: + expand: true + cwd: 'coffee' + src: ['**/*.coffee'] + dest: 'js' + ext: '.js' + + watch: + coffee: + files: ['**/*.coffee'] + tasks: ['coffee'] + options: + spawn: false + + concurrent: + target: ['watch', 'connect'], + + ) + + # Plugins + grunt.loadNpmTasks('grunt-bower-task') + grunt.loadNpmTasks('grunt-contrib-connect') + grunt.loadNpmTasks('grunt-contrib-coffee') + grunt.loadNpmTasks('grunt-contrib-watch') + grunt.loadNpmTasks('grunt-concurrent') + grunt.loadNpmTasks('grunt-contrib-clean') + + # tasks + grunt.registerTask('default', ['bower:install', 'clean', 'coffee:compile']) + grunt.registerTask('dev', ['default', 'concurrent:target']) diff --git a/client/bower.json b/client/bower.json new file mode 100644 index 0000000..e56b1ee --- /dev/null +++ b/client/bower.json @@ -0,0 +1,22 @@ +{ + "name": "bab", + "version": "0.1", + "authors": [ + "didier.amyot " + ], + "description": "Easy home to all your web application.", + "main": "index.html", + "license": "MIT", + "private": true, + "dependencies": { + "underscore": "~1.6.0", + "foundation": "~5.2.1", + "angular": "~1.2.14", + "angular-route": "~1.2.14", + "angular-local-storage": "~0.0.1", + "qunit": "~1.14.0", + "zepto": "~1.1.3", + "angular-sanitize": "~1.2.16", + "fontawesome": "~4.0.3" + } +} diff --git a/client/coffee/babApp.coffee b/client/coffee/babApp.coffee new file mode 100644 index 0000000..c4ed33f --- /dev/null +++ b/client/coffee/babApp.coffee @@ -0,0 +1,37 @@ + +root = this; + +angular.module('bab', [ + 'ngRoute' + 'bab.filters' + 'bab.services' + 'bab.directives' + 'bab.controllers' +]). +config(['$routeProvider', ($routeProvider) -> + $routeProvider.when('/', {templateUrl: 'partials/home.html', controller: 'homeCtrl'}) + $routeProvider.when('/login', {templateUrl: 'partials/login.html', controller: 'loginCtrl'}) + $routeProvider.when('/about', {templateUrl: 'partials/about.html', controller: 'aboutCtrl'}) + $routeProvider.otherwise({redirectTo: '/'}) +]) + +root.filters = angular.module('bab.filters', []) +root.services = angular.module('bab.services', []) +root.directives = angular.module('bab.directives', []) +root.controllers = angular.module('bab.controllers', []) + + +root.filters.filter 'month', () -> + (text) -> + return ['janvier', + 'février', + 'mars', + 'avril', + 'may', + 'juin', + 'juillet', + 'aout', + 'september', + 'octobre', + 'novembre', + 'decembre'][Number(text) - 1] \ No newline at end of file diff --git a/client/coffee/controllers/aboutCtrl.coffee b/client/coffee/controllers/aboutCtrl.coffee new file mode 100644 index 0000000..0dd93f1 --- /dev/null +++ b/client/coffee/controllers/aboutCtrl.coffee @@ -0,0 +1,4 @@ +root = this; + +root.controllers.controller('aboutCtrl', ['$scope', ($scope) -> +]) \ No newline at end of file diff --git a/client/coffee/controllers/homeCtrl.coffee b/client/coffee/controllers/homeCtrl.coffee new file mode 100644 index 0000000..8129786 --- /dev/null +++ b/client/coffee/controllers/homeCtrl.coffee @@ -0,0 +1,8 @@ +root = this; + + +root.controllers.controller('homeCtrl', ['$scope', '$http', ($scope, $http) -> + $scope.services = [] + $http.get('/webapps').then (resp) -> + $scope.services = resp.data +]) \ No newline at end of file diff --git a/client/coffee/controllers/loginCtrl.coffee b/client/coffee/controllers/loginCtrl.coffee new file mode 100644 index 0000000..b0b2323 --- /dev/null +++ b/client/coffee/controllers/loginCtrl.coffee @@ -0,0 +1,4 @@ +root = this; + +root.controllers.controller('loginCtrl', ['$scope', ($scope) -> +]) \ No newline at end of file diff --git a/client/coffee/controllers/topBarCtrl.coffee b/client/coffee/controllers/topBarCtrl.coffee new file mode 100644 index 0000000..141b524 --- /dev/null +++ b/client/coffee/controllers/topBarCtrl.coffee @@ -0,0 +1,9 @@ +root = this; + +root.controllers.controller('topBarCtrl', ['$scope', ($scope) -> + $scope.$on('$routeChangeSuccess', (next, current) -> + $scope.path = current.originalPath + ) + + $scope.path = "home" +]) \ No newline at end of file diff --git a/client/coffee/directives/service.coffee b/client/coffee/directives/service.coffee new file mode 100644 index 0000000..5e67f24 --- /dev/null +++ b/client/coffee/directives/service.coffee @@ -0,0 +1,31 @@ + +template = """ +
+
+
+ +
+
+ +
+""" + +directive = ($rootScope) -> + directive = + template: template + replace: true, + transclude: false, + restrict: 'E', + scope: + service: "=" + controller:["$scope", + ($scope, $timeout) -> + + ] + return directive + +directives.directive("babService", ['$rootScope', directive]) diff --git a/client/favicon.ico b/client/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d828c1276d235da50660f53a954a73c041c09132 GIT binary patch literal 1406 zcmZQzU}Ruq5D);-91Iz(3=Con3=A3!3=9Gc3=9ek5OD?&U<46RjKp?!c4nA0YZgO5 zKmbEePY*+EY%Ifq1q&Do3kw;nt*sdp6ciXFB_$ag9UU3e)YKS~l9CwCo;}O3di81s zUteDaW@cuF#Kc4f0RaJqgoFeJetv$2`}glNELyaPp`oFHft{TlYUwC78UmvsFxW$Y zp9ADVNe}_XJR*V&kWvNAg%VtRPzH<=hcTcuFO-H-Y|Kywj6y1bz(U+$5^yV_;zT|I_3I0|NtFlDE4H!+#K5uy^@n z1_lPs0*}aI1_o|n5N2eUHAjMhfq}im)7O>#9*Yz|6MwtlzgG+l3Jjhujv*Dd-t4Vh zqjNn{|5tSA)x+;>{hz-vwki{{-q<w+k>i3M7=5i#rM&D3tlROW^IwFs-QpYr_Kdrayh%-Q6wVv><3@LS7!<=FOXD z%$#}f%o(0#%aIF-9zWj@#Wpw(KvUmY)*du;+r{6 z9E{gXA6oFW2z;>N4-`51{L8M#7N3e4o>cF9f4ESlVxIbW)h?x$L=J|6Sn)uSgWqdi z0$CV1njGx-4-2df^Zuu1%YQhaZe56$=Y;5phy#}{2~C?eZNr8Q4Og#*u35YG;kMg( zhhJV^ZfI}cKXc|x|26B@{cD&jDt2Uzh{w9Py;65~6f(bl{kr1i(&-=GY(D?!sCfJo zg{>PmHl97}Db?_y4+TQT&X@Z=Ox((xs|%EQ^n=W9Lo^ z+cYiv`nueG9}aPMUtbq{c=LI??*D(z|3C8iynTQ6^>v`|HqX1`Ai!dHLE*qh8~;sO z=RXH%O;s^a{p-)*x~4l${hRB4$N7uzU$s?#z;if&Njg0`QNoEsQBsOs{ol;6)vEJ_ zH*Ag4{jpD9p5a2))+gyNEg2Qq?fLowMGlt6o;iO$xwKStx!>GFKR-V&d_K4Q(be^J zrXW{_?|CE|5golb`)JdQsU|-kiSL()-B%-No`3I1@p;?!^|f!acN9EiGBYzP`1`AL z=Yx~mTojyEiKe@MnX_3d=xfY%*PStawbSkeONyt5&Y60#Qed~zg~$9%I#qu2m%e^* zp0kBBgku9o1;-VR7g1}cu_*qU^Zxgz(qA!~Z%$!2#^`_io5cyi3zzOceVV#v&6)?I z>nm9IJ&oSSy1we_YEVirPCxfx-R^f)Pai#c#A@BF+u^&!=IfQJZk5cWI2O}rEvt$%!?0UEPPlV_GgY%28PL%!>soBj^PvjC@ygY6tL#`-%zOUuc>SN_AAj%v|GUihsZO74)1Rkz-`ACY znb=>$!p_caxOIJq)>oSyRwpFI*kwg^*2pB8hdV^yb+9-b`o39!Y2W+D{r_I{e|XT$ z?{dm*mCa0&=Gw)*DzyI&;{kiPdnr?iY1WI)m!}om>&2&~&a|?XydB*Gb7M+NhteYG| z%Xis7@p)L7ufOkyl3e|t!qdleI%MT{R(wp-F$m^pEZV8_|NU~aS2qqSD5R@Kmm0s^ z7qG11{qGMp{QKTNo|!&xV{^LWn;$0*9&D`f+ZZWwaoz2|l@ZM8EX^C3wcD9jE$MYS ze&$EdL$d3Pop zoXC>UtTV4MZ_(ou4n9*4zT~-_`CvhlpzFKW5nlGzL9^IRJ40XDo$o)s_}|s z@iZ|-XkKm%IiPb>#(w&2^ZZ+#^P4NmIErrRMx?Uy>D4^y-&Aqpwr7GH_f++i&p{S4 z4lABbpCx@V(rwnk+0RQ?t@QjOb+t=WOX1@xahv0ln$=XNTrSjHG%tR-RHXmLia)k| zW|1owt*TV;ZrGCK_~yjZZMXAYM}CPkVu%xMYSNEhE0(o&R*Jk#jD^KiFVzq&Q6FF5 z#HuPOP(?Fu-mx!VOjcdh%GxS*>Z#GPWy=CqUo|i_ZC$xiGek?&qOUn>t=Q@3Z~k0k zmpxv&gxM~7?(4c*0haziRxYNQfB)A0*w>%7xwC3--`~HrZ{ELu{NMq@*)-wbzkg?# zNZq)7dvdGf_ix`o^<&y*&Crs0;hqibTRyvK?(J@v_en8&ZP>E!GjAU}H^(v=62$AP z@4ilPGS}Ut6Cq(QwoxKU`e~6Q$f5J+AAj^{(VFG1Qv=1t#0;#hyN@1qWm0nt4WE2P zCA1{9tfXxHx$J)D$?L6qvV70HdhmeZ_wV0v?Cgm6 z_~TQx!#8Z*+8Vt*@8l}6taHkhBJQ&FQtU}vw?^@n-z}X!_lMY#zWzP`zOEN9zfEC^tAOZyA@x zpK8xzf0o=Z&kKIM{&nBc{mzw3i;Ihux|B{9U41OHAU`+P)!+WF$-j^C{{{PPze&W^ z{Y>RN6Im(HdGq4linG~q&Bf2pMVfD7-S}8Fzv}Yrf`^A%B^*)?9((oGYSmS(cYD9b z)jVL9KX91ee$TcYJ1Trx)tP)nXOw)?zH7O3ZnhKy@2kK!$IC7;w8RU`$DjYaVbdn1 zeSdH7m(8!feY@cAudgri|9{K>IKBSo^dHa6_iN5oio9X7GxyEHcDX~-_y0+?eyQ6b zt6%w8`hJJ~|IhZ*yL)WX7E3k6e_a*+Fund;`p3EVf6iSLwf37%4U@d=?V!D#o(C3w zHdy7#?;a{VSt4Tp1n$@q9bA{z99a0{qx}EEn)}~tOH2FEZ2zPA(d7AmRPKGA_kF?59Ir6c>ULK z*R4^$DaB^Gnw35_=MP`HH0j5Oc6*(==jC&D0G>F4K(ZogX=t;3fku#_=T&5vCETk+#z`%!oMUx8D+mPYiScAR1K{?(UV z*Y}>|+r_%^vG(`72;j@-Y*W(;(fAn$!`zoQ+vPP%T@;)1gg}Z^!dj2vTfbD=lkCGN2~Au zj&pSVG3nLo)i0gG9wi*RJWZowTF7aQt`ElcpN${=`~BX1Z(M(=p})|k{7Vt{3y;f| zZP4wIjjuea9oOi!RH!>u>d3R8l_4KC+yCDDsXnT}(>J0V~tGQ-H^#I7$1DQoiBgF(dohSU3y{R;cpwF1g!rRy}Pq> zp+t~~YeZyZ=gRh7tJa3`se86c`p>brD4aHN%1#HLcXxIcmpSNm$ktod{`zv^!C~I> zF}9mtC7CHKOLGa}_l!HTaXyPZXF+Mk`u?9ckhmg$%HqrNm(4U zDtXuL-K%v~mn5G`5s!|F+SHus_~ysyKY#vQIJn|#)n50%8_hPpX$ji?W5&C;Z+Yhg zI(|Q5#=cR(OfYEm)wX}gYu9xQmmMzvHU$xC$)(cC|o;hRlaesJ~6)Q6P_1_qK3vhu1<=n-vaB z)eiq9_4O)CL01TW6GN{XvlGXa*IzsO_V3@XZy&$@`cn0E7c)-$e|hKIw{I6ROp;#u zguIT+-Dc=>-$N+N<`84!tmz*t)@5zw()#nlqOZn};nK?+U+le?Ui$yEGR{SHvF!0q z#z|2Qfzxg3UtUsWaTLfdn{$obM3eVS7wg7k-jpYQ7ad!-<;9Oj#j)bj($W=wJ|15j zw6bNkdH$lCIZ5g1?2ZBn>FMmfZq2jO7hKF}C>P1v8g*&+%^Wk;h>3H1ug_9v-a4!L zq&NG;&rANzJUnfA*zFr1k16lER$%jU?sUZ@g>Ug*OE1+g{HQb4t4O9Yefi3jno~}v zhG>bNIeRuSE2}H(JE%)&ZEYP96Eo$xFbiYN{poY3zHt1${$>u}l(tXCo+B&%E9bX(w~B;H`vw`u ziHekL^vjB9JoVy7x<#^#*pW#LI-Q=cPGwz|SaI?5pI=w(rdoaVe;%;#y}1ZC$1IQ|GjMAYyEzY@YX2a&6_uCOg}wi-n?U1u85?irP=M+ zv*(ZDZX=xz+0K~VWx2)a>FI^l)zR-R-M)R>!ouRf+1ciS>#uLzy}SGF-PoJ@8?R=a z7MY1We12#*)5}@UKY7VC zFR^<$?{!F4)%@_^0jrBuwsv?2|NIfp*JR^9=|I;pvy=Z$bVpx`f0ZApH8o)U_0%{u z!xKe2d$P86y?mK@=FFJ|QEQVjGdZ_L@j|-Ahu^%>dG_p?fxUhI<>mgjO|u+*^sldv z{~v$*_U-%ockk}*?d1hEQm(Cy-ckBmtgO6TH9%HICZW7Md{z3ZCgrvzdUd56gMy=j z-Y;9e{NeAqHGS2S#Cx)0r=ETq!Je(P(8o-A?ppWMG#L#JpPlvUYco6i-d=9>+<)uu ztLH5*bYGo_ocgZ(N?)L7kN4j_>)0JP-)wok%gBGyvB}e~FXU~JZ@(>5S?FaYcUL;8 z=#TXui!at${yw)pcwAL2c&re<`f9Cbc-n-Wp z)VVJ%7Phvw-cj~es;sQ6;Po}#oc#RLe3yFNoWmlX3eOI)mK7-Y+SV+&cOA2SlHZoy z#iG3qUa@yp3C@x~UMO?rb?KQiXHK+j*mXZVvTxT3P6nCd-YjZ!Pn~$1{OSi!mi*)O z)_$K1OIOMBw=0+ivs!LVo6*-=Sgm5KKmGKL+qan;=5eimWmwsuvvOLApU7MP@3FT0 z|75M=nF2*W{CNN2^TOYLTc4hKYLvBADlIJy)CYX}G!>K)N=rpQfBqa18+-K8Bc;-c z|7)jCoys36V68MYFFTt%e0|)}rQXvw?A+NYZJq}jBmoVr+`KvQo>5-#qgsxgzYg0U zZtT=oVKTkfFm1J|%!ez33>uMckkYnNLJe|#VGbaZu-ujwl96}{5!@t{r}VBN(+`0 zKDz%j*|E5=tW3U|Ilzkm4Ig2Z+df}rZoS<(1kxt>(fcRq8jpA_?io1%Qa+#T#fnxIXbp7PoT3Jxs9z58%#c`#= z(q!u|alcEqvqywDT&i9FTXIRA|KHWjCmk7dIiFZec;V2|93r(@YDxZMgT1n)ZDn^Y zc@F<6d4A{pZ^f0TIiJh9{yNl}d8{V(jyOA8=+lt5kuoA76JFNxo$PqA^~dw5x%&f^ zW~jon-UaAE>nx6g6MIn1qFg7ykN^d8h1lZbWqSH)5rMDa@Oas9Bepc zlxInIHpnb%3*lkT@La#NaYo<5kIz~Bv>GnP-_JjF{^R$O`PZ8hE+hwEZj7;a2o4Qf z*S9M9Vx8`UHB&>y=+&-Ov-X+aM-3P`{KpF zPaU~(-BddDG<81sPFN~j4ZymFZTT*&$;J+5A{h3D>NwDs&G{K{?!!~qDQVIFWsnjwRz@| zLnYm2nG;_(o|4U+?AcZLN<3^~mHY2XN%q8~=7yMlUZ2B99__b|sBhfTG%0hzvQ^Wx zItq_1V&)C?eUloxR-&Wy;MwbS?(79t%iQk!vq${>slInT^Cyk2<4&KF1y@WIpL(r+ z_0^`w7BgngK79NBzp_1_=e|Gk``&lEiQFA~KOEw|82|5S{G zn)2-6Rn>n|8#zqRx}{pm6ijl^d^_>m&qPO`@;a;C;Zb{TC>;B+WBY~I+(v&JvL^g* zXFOO}wsVK?`Ip=wnG5{(zf;+?ZAJS`zEdYAG5E2kG_Q!aD)kFh4?XblZ`$+!9^7G> zXLMffRS}S3xp3wBj>itiKTXOhh<)z9{&M5J#U*QlR_dRbW z&8pb%buqD5njP7(=5Qg*kr?_Gppr;FRz^5X=ZNkB=K|O z2Htlo0_O7DELr@wUi+Kvsi22)G55c#&NzGK%!9x0>+S2_J+I4K(9_!J@aFUU|Ic5T z*MBzu_-6C@MNwr{?S?SkaIuiq7Z>IHI_HShQvEb%s=~Y^5xwkCkVT<+f5ZL0_Thzdr{i4e5%R>H#&+Gp^ zKf2K2%|-YBiobr&|6j?FnDcRa<7yqp4x#9mmV5npx|(OMyVCP`TGfqEpRFYeLcfPx zkiN2E{jv6y#{(I?#ow%2)p>Zy-6b1YZM}e@A`L$PtGAfL3^Zy*nf8bvK#eJ)}#l{~OzrMcy|N8Iy|7+(S zl?wlQ>gg&Lttx>I!Hhxy<+5XMlQR{T-CFDwC+<`7V9K+VR~lo!e=giw5Fy~r)p_A) zq3YiCP3?Mt)?Htg7X66VW^2|o{kg)EXY%r?rI+tsIvkTxr*BH;F&Y{j$G<_HK+6I@7XMI zRkNqd9SJE76tA2g_9;zbc6gM8^LNvW!a|9RZ(cb1u9cW{#_nS9vsus2eA^uQsijPA zlS3b0&`qJkrHAw)RxS``zy0pkga7S|ep@olQQ&yc(!t2jt~4*?d}HVGa#qLnvwR&+ zG~O(f0gZ+|mj7?jmXy?Q_e*2h^5swY%Nf_i%FS?C9klX9)=ZY9FVpMw&vN)HEZd}- z2WkrnNl!uW|0>%hmfP z+dj(L#?9z;sqnH7hmu#pu9iT5y>(m;S$j>_DFwY?-1>y9iiYSWM`Z_FCE%qRhGHhjXZU=6+TR-@ zdMq;?diYn&p0HqTo0ZDVEp02ytky`)*jly7?tOUPO)o!^&;iUl-0!$yyq)_jHZPgO}6S8FP5> z88Kcx9~ph);_V}Rv#V!KOEdGb%MM!iTtVvagIBAWDhjF^e087A{P#Q0U)FT129s^R z+3e20QNoTrQK5oAmPmc^ej3ry%9Yf-s@gN~-QlYKlJ~E9Kks$>-Ei?XmV@A}REw%4HI?ZVQlPAE^ z&D=br&-H6d=#*6&)3r8!;L7D&a(!c=CEo*%1A+p1-z$Z7PZZ~7ce3bExb@c5)8EARkmJS)gQJd;$-lgz!i5BPn{Nc6}5i0sCF3J z)jO9sCYIl5R%@JiPE`BC`WN#>wZn8KbneS~8lW&OWNmP2n&;isza@Y2pMJC^Vq=kv z=%KPcb=!XL6P*_y1-x1Unqux@W{Cn#H?taoC!Q73K%Nmh0-A$%JOi49*6E1cm~_;u z(ZMJDq{+&&R}{8By!>k7=j-jhGTRH&!`ASq|2>^Bdt&0Ltl*0-x&4iabp|V=QumjA zY38}5AZ~rab$RINElEec>VphV%<_&p{CC&U<+940!Yf2$)YGd^cUW2a+`OB3^YVt~ z?4w;%9~tg-yj~L&XJ#s7ImK#b*!AZdLbcf5ZZ$vQ8nrQL=?|a$fMd(FyL~OEt_}14 zEt@ZJKw?Rv>;6qPi?_&1v+ZCzdmn7qGL;qwF)M$e3A{QjxnlvdcPv!^9lO$}Ub zwye}*TSL;(Qy-JU7iWaD|6E(N(`Js<%#f`~GdAtM^urpq7GR^N__g(h8B#~zqzi9u ztJMy3V_y=ObG~`T+(~DGGb0{U-YZa87IbRMx!YOBG5@B|O8BO|=0<E4M#wc&*8~LfSk zKk~Eh%ssB)eTY$E%0{Uxw)4yK-Cj!1N_+hDbBEPg_du1mR;$*!vr66bFynD{Vfdrq zAi%<5{oR^Lpi@AR$wkSjgP~y2x!j!pr7aF34655(6gVDO^u-((*(Si!D8pQ3%fXZt za{J$3g`Qgd1;q=WPrP3#XTPte#Lj>J``>lxJzjIqn^-@WW$JifvB)^g@p~fh-E4EOVfqZ2_v@C} ziOVhswl*jfo4Jm!ePwqOGrz3&^+wCwP3o4GB@1SWm>OY zXTABB&b0Zm?3coEzVN(dy|xUf~|k&(=JL<(>(&y-lv1r*7}u zuO@FwHZ;#*f{PWrOeXcXgxF0BGZRvQhq1}PQh4F%BtSno@Yl~b1p|?rb5(T!f zw^yAnEqnWoPo!Q?Mrs2`po&1-oLWl(rA58+eaeq^PgPE_F1`1<@BNZ3%_|-lpEx&v zi^b$kf|m|#*!^J74V8z|EnEdnx(zB*l{{-7bQeg3U0Ut-Vl_+gvbkn_d+)!#w|`4< zz(LbYVb5LlyNWOLDxc-A^MCRANJ+@DqxnmF{y< zZ=0-R`Q?kH+N<{VJ)c+2XP$TGz*6t&i;UCHbu>EgFqD;*eVBQ^hE3gXO=4CS*VZWB ztx>#}FJE2}q6I=%vsQ$y)|h^JMcC>Yvt~K%i~)@sf=4QimMvfY(87=XLkwq2%8JK1 z-nl2dcZ-J`UVZl@P~=EX(W@oPThGm8*Oe(f^d(u~)%VbA3%6}}{@mgE#3OnoXS8xZ z@;{1M=xKD)$YWQGP{PXt-hbEa@_b>=^f&$1fk!G;KUHrhuJL7c`FfFgixQIyM@PV{ zhFcw%*)-LQtmbnwK2+dP;yC{DkN$%lax&r{Y85yX;`1NJwVZg*8hrn|>J78mr9bxd zhpkqX|6*_G_{&@2PLA2d95X={#=JW_6wR|@4`e@jGjIO<<7a1^Kis9aE`I;AROP#| z9ER57+p7z=_B-@4^v{S{8#ej;>8G1mJgUx|xss$137Is_+S=u{v}xr^P0pSpLNE0j zlU9aII&k>^bK$A0nf_D?uo$wtPO^20<(MP7a+|?jC&`ehuhRERAA3C8{l)1-+nDCM z{r4ky7KQP>3b|xxb+_%Pz4PqVeLr`tyB!&B5Volu5CL#pQbG)AScNjtF=!6#@;w z+S>L8tyGa*{=D*2#23q|xik0}Eg0n(jeDj4%KeSkoBmsG*AmymH|JHwcb#eRsog(6 zcSF1e|8_yX#gAU|d`h|e&>?93b=77C0R>URE+rWm84uGycA>9Tu}vor9%Pj2W#i-H zd-7VVV8!~YufN`?W(oKrRer}ax$b$@i#?%j$DhwXcW-Upwq5^s&fU8@bSvY`Yi7NT zuXp`8IscERj=p~Vn&8#>2&*T%1 zQ;fr|RxVt%>}rWk&vUANv#WR_XZ zU9R6du`pV2yXM93HTRDnJlOap@8X(Q>awNBhwQ+)MyvMrR121t`LA2I$}D24rdp-j&&tB*6JFU~ zwVeCI@TcX9>nmqv{`kA_`}60|KMLD_6b8*{u9+KBoBrDPyp8bR$M*k}L5p!_o9Bb( zMD+LnG180O)l#*WPtm34*rl@%f8}0ZswH9?QZFczu*27fRbXoG(n_Vu6>RY`TNqx} zbOaXW2MIm+bbrx#LFdp=&%E^DwA2{*IGB)^>3`G!H#W?=XW|gu)6PiQR=X? zDfU)zo!_eMQS3Taw_}7nRxMbboDx$VZ5SGCT~v21RatKulX zUt6veCfoJ=|NH-cU%tqW|1Gu1Z%xh%cH3hfc0WuwE;We%5PDN{-~EAk^_TKnC!^Mi zDa4q}F8x_sb^iO`Z||gBGt6$U(q>%t+|sbTVDZHXeaydjj-C{#Ui4Z0+EOD^*@Pm7 zhifL-x$78dzVrRd`?WkIes=nOQJ7?$akyCgw zTbf56kg3pL^QKE_Y!4|6l9*TfN(hqFWO?(l&Q; z$X<2-axDL4(yor5J5=@4-fLABPIyq56eyy|@iMLTmh8n+8A*Sq5<4De6Djsj%S>nc zRwkeH@N9@)fBk8n_bSFl&HsFF6H>w#bUJ=xWpevvlkOWMyWG=jRmATYX(69hffd(Z z+ir6fonle2sxW6&)jA`0yNi<}qJq}MTwl%h_WR{S&R0CwT4`RA2n+fp$u1eopvlfK zHTT2y(DUN#mb>bgRF#{1>$6o@={Cx1zf%>L{k=jw`R1+z`FYzGoWB^e_fj#S{8OhTJi;Gg+4i}HalD< z;c*{V-8z;%EEy$!B8@)xv(09I+MVLV+r%*Mx#xR6U$IRWC#>z0j_-_{d~sTHS61;u zt(@z@B3~L#zI(Usn)C74N1i=i;+K~-1@DcMzag+L*!X1M^5CPF_Z<9FQIa2efuZ5$ z%a`-+tFZIuE}4@!DLk4rcT?&mTjc;{e&t-J7+;m;{D1Fn^|dmb@YGE2UzE;^P|m!i z|2wx{+}pE=qw#*I^vhb&lqF^&CR;zImB@$mr!Qeuvu${peEQIlDF?llPU7#F7%16g zyCOcNH0a7sy-Cr%&)&MZaGyMpkyLn|e|xe(#LqhM7|`UKl~Cghy|s^FlZCva%hni$UNC_@QZ^WlPl~Yn+6%gWmdD&2Br_F-PpE-?yG6M;9M!eA$$EIcdt3FFH)3^Nvkgl2)qT z=V4M;p6k`b8tU}#{<(;Ny^UEj+=~R#XHTr|RABCU;8rEru%p*N(&N|7pCt}crghBO z)_7@S)Y>qe8@pYo%EVt(_RGEfZWZ`B za7875!{wRcjm;W8w+={J+Wg!$*}~gF%+tX86yL?O`%RAbSbbl%eEDZa!?^41t7Em2 z18s{xEmGW4CK&6t;GV|oQ>|C+=B?g3b&2nvC2uNZyDwCVbKaj_PZ36 zF8AO3TeX+*e!!18;kkuY(n;&T`z>+%yZSu)%EI|;_Ng=X_Pykv7UG{-qh#7PalP!^ zolPNmrm5QIYHr(>bI4BBR-H7nr*!Xb-Q71B^SZL8u(_zq2OSXPlX-Yz_OfUVPoM79 ztE|2%>1KweCV2K}_Zq%9=O0`0wpwQU;w_)g-DuOZe^}=Bwd=>u zl?;=ZHaW}|N?9|&?;#N``gU3%iDy>s!cfETk~ zZVzGpzkyY*RM_k8-`$gsybN@->}XlzOI|L60o zk1zdSbUum2dfEjWcag7qj{g_ej60vFx#eH$w_mvip0lqjv}Kv@*!o+#Z;t&O-NIwi zsw*A_iZD)OW)$bE?l{rdA)_Gjsq#JZ$M%Bc&ok$3&t2KKWiqSm?Z|#!zkAoe&Emhf zE_>RRhpLu~Cv7s`amv!zcrt&-#|LJhaSKUTi`VCRI|CikE}hE;jbJo$UGL*d7Pc1Y zsGRpbdRlAb#w1m3MVpmjtA94!ICbh$`x>vzqg_+iFyu_>sC~Y{e$BLe&?v~%K!+Z$ zd(i@$la8K>@LGQ6Z&;o}*^HcGkR5@pnk*kS-f`%e9;J6Hf#+8Lg|4ZOmrVL->Tv#Y zQ&smeziFpI>yc|T!UZlrJt>-KkaMGbswcbg*UF&(mlv%Ojf*!h7x2z5DZFCtfBf8< zh?y=5-cj7mTYjGoZnhE<(4LeSA3pzn*Hq6bjy_thSHt+aHI-LRete)vXGP>pkq=(c z{hnVhb!}+V zy6o!p{$IB)J!{Efi!Ca6dP;Q9heMxNTz|dd`fJdX>D=;rouGD9`uTasTe-zQy#N32 zeMEfx`!A;%7Ad^h5+{=Ji;=~Nqou%QiRR{vXHxvZ2a{9;P8%o9zrJOCh}Nd5Goz|s zyj(v2*yDctzU_OS>Vnp)?Y`SLRqVT?z=Ei?i-K03==03HJFTMg{$hcxp*HauTP_#v zX4ii-V+%)ru#GIcHeX8C(o0JsR&IIJnYH!KVxx&4{Nk?s>eo8O=GaiQvuE#KTbrNj z;}3D`?_pSA`#gKAlvFPpXk~HD*VXYyL9=vw-q-$Dm#cWd_~-8Zzq0eHUTIe3MQ;16 zEIsS-4u%!t%O{_fviH&5IPuEK!~YV*w&^hiM%__8e^@yFkJ7(~#`6!d-}_%z^Wg7$ z_oXpxR=uS^mn@stS6yA5VIpN)%U)W`)U`-Y#dJ!>bh~BmT;*B{Qbo@%iH21LCS7~qaJ#4Lg~X+;7K@MWuiwyJ|Lghrhtl_dupPQE{Y;zrx!kQ$KPKD% z6s~!byuUx_WKF+Y=Cw0+zrdpaU#DYiM}Fkp%G#QA z;BlqgzMp6FA92V3V6ywr$o~MecJtst#+v)@4W>*BTYKa7?Ze-`mE|nII#o%Jsbi&% z(n@``#x1uxHG4B<*aaRxSbER>>%G^zCcZw^D$aSN!nW$(+Sz9mboVzNm#gMEZ}XWa z#(bZp>1tWBW=PB2>)WdNjL~YMyHU?^-_pADh-xp;cE`g%h>7W`ycPN^Ok~2d zo-T(|pKbPhx#XR@J1pJSWVY{2p%amomg_v$mc)wJe-y88yuR<-)*18W9lCw*n_0~x z;rSW;lhq$=|D^w}AVNS|Gs*tE>pHWjwINy+4_n0xK0b1_`EY=F$J=e6FI_mPuEOqU z^g!5nVe;KQmB!!pE?OC}>)Cn}&F$N_7d|}18ndrP(%Ra3Q>zU7>af+SH{GK)UnvyZ zRlo23W9|5#T%fhO4?iB4KYs1nG!_P?)qnri#_X??)!*}hX6t+FQxU53Q@l4^IwR`mXZV2!1-1q*m{lCZd2DY}npwzNYzC}n z=7rKVudnHv%s$&#WhAG1`oRNX7RGJcwiP@)#QNs_d+*;1UYUmdHvg8*WwXv~W6;W$ zw9TEMqD@&f57{V#J?&o7y~?)9V1^Z%~-^QM`9ALrGq&{-)p_s{!2Ui5pCcE{Ay z_oTI-8oBtrUXWeD8uKur_%T~t&BMw)55InYbY^C7k@~a0Uv|~1Jbu$`mDX$h&g(l3J0>-~|ud`HFV!Sgh0{A&{~C zz3X<*oZbI3rd!tkx;+2bbNm05TI4R% zI~wFo0@qbke2U-SGVOkYfL@crq_|AeYZGq=UcE49dVHN^{LfS23M~ibmhWTB&d$Co zw|XA8{TcO$JB4$c*>}rTb?WKq#hvJ`YEO*0y7-{D(}R7#-@X3QX!|Fndl5&k8#8F< zirQkmhQ(91`JQimUb&+zS5{mRTJDmt3++mp8q<=<^fDlO=usKk)x= z0OhW$SzFcP@+Po~urPiD?+J+as8(z=l6I;`&LMnVjG66!SMJ+>5QeFIfmOd<0_;>#QAOD(vpXc}QJ>KlZK9{-Ijd}O( z-5F-Hk0vFZVtZ04rx&xM;p|!8DPHdu9-nmVJ*a@rySs~(VaM-xyO##*od505x8jbY z&U%%Csbcwgc}c~^!sT~M#k01)I$@cRl6>Rl&57TdlWrc_m398K{{Eh~Z*xPmL@Vzt zeshQ=Wf#Z)MH2r)`;F|6zkQo)^XY_g#@476AzB}7{G~p%N8U*B&oP^Q=~#rya(9U~ z0l(ii-@knWrBV(--rBjlHr~B^_b6yfmjTblUAwxjUJYeQ)ZYGjxu5$~ucyL0n{TY> z&ae=8y)0_Y`t`~jOnbgkJ@aW|Db)P>)GxXW-|w6RzB*3y*r6 zIec1^o0s=!<8iseU%qUq(dBWTa{B2)G4s3C?>I8dW;?H3{Oo2%`Q^y?_~ZBP`RPa} zU*Y}p=xBG*n)CDR{k4_1zm|4gEiHNE(~greN2Ge$Km`P3mD(4EvxyE<7UYhyJ@ezoK7DRp5r>&Qjc-ru@7H<$)ZqGz6tx1G2hYyV-ck6Ntwq41@{`J{ zt66ypc3z*ppHP@~)z5(EslA28fph2h!otD~Y;C7{8p!Qm+3V(f?5v3A+^Ua9#h<$5 zgzH#baC^XiXio{BK-VjMrH+qUY9)SOt7QLw(*NJ&wUp_2;e`y7fVE*8_wVPwUK*%3 zoqbzxpp2M#{|PP)R;Hp&H)D4ex$d*`+H12e=-TU2Q1Q+HT7r4==FM|bCyvLMc!kcH z=&&Yi_0zBt1D@Mciqq5Cf3xqlD1PRXzP!U|wn~cl>vsqI{B_suIWTK^r0kX1JN9kl zJg&=l$aS^oiMd5L9&c0E>))JZFRtXYG^j~H@X(6C0)3bJ7F$<|IN#VfdqU&?*YW?Y z^6u@?w0-iV_le`e=2_MyhGiu@^G+_jE%1qPXWpL2_wLD=nVAKw3~5W7{py*^8?&xk zJnwI9&1PuuTH4au%F5zcl2LrLAlv%LojZ3vhN)NTNlV^){_NQjUKQ`DQztF&-thU- zje9!^nb(GSXBV6=68xC*b5Z3p4c4}XmoJykf9LtctW>`1(6Ls58~5(HExtHmc3#+V zzQX}+kr#fQ@)nj9tTT2LA0 zAh&IrqngfzJ`Z*2xv!MY7ft$@aB9)YNg;6^4BbvVo6hC>zDizT{N(vsub>;p|NYx1 zBsWnuc-8gSMr)F?vb3&u{5?GF;YpSwpLjMZNVBfAjnqwvTi(<2yRvykRP3~Q`T6rxeucl=(*L!<;ITI>P)@XJm?!TJ#@WF!vH*P8A>oi1kZ?`$a>duz5 zTwiQ+)&0UX5odofWUV{WfSNbOHC|1))=3 zKcB6=e)aUFVv9C!^ld5M6tz~V=TD59VR+@HbGc7?%U9kx8Te-3-CdD#42@NbTmLq< zr5Tkz-gD@}`I?Ot>$o-MU0OUvgXi}1mTl~BA3Shi^GMr!Q{`*)1I|yC%(DN_rfs$> zdGvk3^)t#}ZcDqz87@#vOHc1N7xvDssk+k=FTDJF6jw@OFp>XY4n3%~B|@M+|^J^91|pLG6$>J?Hk zxlw&OH@1do)hfN`^e+>bZ0#y|C6sBS&EpRxXO?SpPj~5kXsvhFuPrfY*_FC;M|Lnj zjA)+myi)$u;iG3CPupQNbNX|Y|E|_EVh>%}xpM1|ZiN!LJ%S%pjEpAFHb38RV)X{* z#^)0oZ#;VRr*+TkH>>aN@%4GMNagP{Nx^lia$P4cDowEJD{fl8?WyH4kv(-P3e%SV zby@ph!q>Api=q~-W-PnIP!h4=Dfe3o)Bm<_o@FT|E585zzVhD1qn%GVlT1!5*=Huz z>yvAKSNeql&#uzHpSz}ZZeEbb{``%B|2NoLfHGg>M3boax9%BAOVzo>+@7C0pIelZ zl(=S<+qIR6SH$}JuCH79{F&Is{oA&w_3<3}w4w8**t(Yo=bAKswCIScO271O;>*`h z+y2_DF7LTznwuhU+L&|Bdk1&Hs)8w5S5&&QPS|U0U9Rys;Z$`^$i0(+ZxWMPlWG?D zHs1);<2lW6xiKbmu^Q`>&m7ecqeAD+o#*z^Cygno()7a9U&e1mj?4P!aKGNGKG{Ys z>tEWm&px^hGtxVbPHc|2=hM@(E3!G_-&uJ!YjN(BU%%};A5S(HKJD$MdD!@x!m@22 zHW^v2db;H1ksZdTRivLPOiMraP9rqdm~rEt=2+iq_3mj!$*%XmlnT9{yz=$Z$E$pF z^uN}ID47^_gla8~2~{{=sPJHu%&v>hHjmsM2?#8FbA0+i0okU^57}OR>rd@6F}KgV zIE(#t(&0}!^Vh82HCw;J(rqihmhZX8A3r7S54yr_ z+GQ+-t+n2#OlyvL@*+R`#&yH1CuM|#E%)v*sSH}Q*K=mX;ks*W?y=VEl(yt-p0w_l z-=)&er*tFBcPR-dFSOWb^Xvh{?@h7&*>h%u_Omgs^VB(-ys5|H<`IuiU;O-BHmtsN z_qfZEx`|~K(&}b2v@A1|lfCu^&y-lZe$R{>M_6|k&9&`6S+wP9tZu_sW+U#Tn`ez< zH@tSaainiim#f@8Pj027VN2BlznV!(uMFcjx!L5`#A@}^Pd};e(35o)e82%3)Kzqu za#P`R#I&&0Q&)bR+qcwc@1E%J(nhJB<&(6GCM;KN^k-x4O<$Q3`M#Hfacy$evB_8e zKEGN~dfd85?U}{7#aZoHldsHYzNNClFYMCsluKS8pBz21>06!J!{)~p?&pt8n?Bus zSy;KL<;IHJ+X`oCgl*cX?wX@M{kz2sCAK>zQ%W`3^&Bj_lykFXr30 z!AGb5T6~D%Q^O^`jq4xpd30nG$Hh;ZtR^m*Yk1mmfum`r@aCWzHM5h2Ny|8x!qpEO zoVjBy|9;uk)G3Q^9X4V%JpZ}JUuSFA!NTzGI*ga~<4*gW?P2`xmzL}2`ATwjYr2tf z>82oE#hx06lI+6LK=a#K!hMeq6v}Lg(skxO%BU0+e)!HWzg0eF=Pe9PpRLM}-FCGq zX);%gqksZO)0y6PSx4$5((X?Q|7tMVQ-fP=+Du=am5)}fj6E!MPnUaUid$BaaPZ#A z%g*l(@aeH)pFJlb>-DcS*84?NJB<2$6Xu9-U^Og0(!lfkj#^q0@8?qoTN9I>7Flk0 zJ@2gJv&webOVP=#zE&MGbsl7Pb*eTNh`1_;Z#Xff^J!|$Q$?|5(&{%Bb%>qlQdyQe z^=*Z}#KoeJ#dd$~v%cpm`XXzV2kX5Q=UPg=gc%;L09 ze^%Xu@*s}ld!DhW_YPI4pMF~8(|^uteeY#|@L3#Ey>8FGvDLk+WcIQN*JYUyfB*QR zRhRxA$e!esTd`_x=N`8O*}{wE#3BWSj~6D@>QuHI{PLw_Lu6yOA!0ryV!dEFGwo>JP%8Rz56 z(U`V*=1bnE$Iox>-OaaV=dB6l3HcYb+*W8^QCTLpl5s`kz1a#JDWz$O3fW&%Bd>0? z?Pun=Xq&b*s`d4*`5HVOTkmfE8SvM$OL%jV>)jZiDa)=M?VQ>5`N@&vpZD;KvK_hd zg)!K)e}|PNXX&@KRx@qnCYzb5&3;~X`h&uBE<=WZl_48;?5J2c;nfn)!*(mHr!1+u z^Z3XbAG3l&!BU(I6c{yns`$9zeaI% z|F2uVqFO2iPo61E4vpQJBB-3!()o1b2cO0WLCKk{M%Hp#!o~|YcKlyXFV)={)&ra@aBE zJL)g@&1Bp7E&s{oP5t7DIy}Ow!)~jtI;@a)IOF=UQ}3RYDtDh1sErOiudk?S zYW(##zv>d!F!kd^&*TfscGs_#ZthGd5{k1}B%t8-D_3r^R=WrjGrA4Ke zO3r&u()pxj%(`(+K&e21;`=4MWpiz2KG70WbL?q~ZEXlN=AD1OHCWx({KC`Y&lJVf zm_L1bw=89m*1JNMub!`qjz;!GIa+!rIV8=JxnL?Hbi{|5!T*BU(IZ`3EOKlOPy775 zV=lSzyky=rA05sdZuX>ILRZy@gr)z6JI*$biaPSb8@AD;iN-1 zkLh8sl4`el=^0Q^%u9_3W*;CX0M>uuch65O!5s<~7Oc zYm%VMkPq3 z)?9t@Q>VE7XXU3K`)=O6nbbJru|@x??t>|JBzRj+Mm?z17g)V7%2G>gTKEyW<86mr z->hG=rl#ZdRW&K84|gs6@BY=8aa`r^yTs=Qmo)BM$?`#EcEq*DCo;k<0!LRTF)~;Q z7;ikG@%62J3)S6pK=dHMCoCk^hSkrO+=sF~hAR(khQ=*9(&277;;6mwEY6aV>S z<0j9&&nsDXvpG)44A{78qH7?_`OikHlO}3*tn@9}oR-WoLG7xi@WGbHdlI^t(k90( zI#KdcbDitVKeC5figxw{t^9KEP@}~9cUg-Ls&DV)RO}IV-I6r(hRTYmt0vx^GPAAv z@9C#Tp!zb*LLk^$ZYGzT$-}umSe%pYjcf*Oq8?7Ok&Idei@@#r{VU6FNMJ`9$162h7 zYCO7r#VBAW^L}LkH^D;_7>=KQB2)3Uj%lhw#((_>X;SUSa&IO|q=~J~XF1{%Tp#Ck zJ!DQq;T5&hc5AdU=XC}$82)v;=HtWuC-P`VDqm9Myog(8l@&hM-k))It;U0lz3aoC zh8|bQR(Id?eu*SI)AbtsJfUQ(8>N?odX6{N`X;6J+jtcUGcJ5( z5ZSl+)r-@cE>9E`FiuqnSKsv5Vq?^U$j!2+kE{{#JrS3k6!+xdkr#m#0;iM1A01qG zP0j4)nVd@wXH0|_3x%s~%H6SY*@m{-xwo%`bhNIt*r>#^`_uhxK5J$$O#5E>!7|aP z{F2MYz*xz7Yq*rs_>%5eXPzoMnEA5syI<7H{TF>EJ-J+N^qWg>m2FQ`EH`J;)+o`$ zC(KG`c6?eT+@1N+hTX|Uw$!N7iJy6q$px3{&FAE$%Nm1v?gqa5(VEy8DAGN>IMzH( z-pa4RsH4oG^vb4P{|tBn^`@(8eyFv-ywm@Cm-Umhw6smZt(()5KBXucnZ8;fn*2E- z$#q3i;?f;jPYW6Sx`d7QE~^lob;!stF8%%z?K#R-Wml8eMrAF%k#uT<|C?zJJq>4C z3g`DlN6eOx{C4$5#yb<2$Ake-%X2~X9xL|GpGv~&-~1@;Kw~r;d!=s1xp<5 z*DbBk(cW^vjzjpg5!>YE8Jdp{UJnfmjrDD~G5HnC=~YXo zigzmhyhfZUt*nOUKij#mIZYOxab4Irv&F{C?Mm7lu`RzCZ>ls#$#Fi|lRQ(ixZ$zI z(cT;JQBhS_R615pzLv6G`LxX%AFhs%Rt;*+S(Xz-x6O-iUwY<_z{=HyPj~4lF`Nza zj{cr8Bg!MTf7NQ0)mH9Cfvp9KE!_r8Yf9JHvI&0qYoNaLeToFjf(vUkxRMU8Y-eh*MP zE1Tgy246m#qg#4zpG!a5bYsP5!|qF0R()gnV#axLQ#E;u_v&dJ9|Tzgt-F*CQ) z(Uc_ji)T_}uAVdr<}Q6~cJp-T-@AuEJ8fY!tle z@(KzP?p#vyVCA=o0ykIG-?>^NvX^)Fb4CV5fvvy&p6$H+`J=9uw5F5oMjKU^7Kg`2 zq`v$rz5m$dK%vab3ErQ+r5`GN}!G@jp9l~d@jI{W_yGehl@YEu)9gV+@1)Odbp z-HDu*TG7)uL(?IkynmhVnKMf^`mCwwt_XU3#OLNJjw2pEW=qsgZcihxv%T;XU<4X%6yd~mb`G8>`b$zBKOq9 zKKn_rZPIakvqN=4^hP5C4waCNC#*^*$hCg=3tN4)jG13BEs8lN)otE$MIPrZQEOdZ z*`9e{Z1Cjds>+7QS<6@6uU;0iWl^I=zv-j%>aDex{yZr?()KYx?d;RCTN680PG-`R z{;Y8Mr)(Q&2&9`gX;1rH&D-qhRcfmvA~pu8oLoIG;+pi+8?JAv=Y8k2I=QB?r#XBc z=jyOEBD@DS#Y9(1eUm+|xzFn5u6M^{uAAO-y^&H7C-v8kEA&v;yjK57AK2s~&wIz6 zxKeT==$fxnPs5E%mkupfbMRTKdCXc)b~$TQOYFW5o{~$=vA*|&r*_ViNxoj%&7bsY z!3+k2f3s4P!qhrDXU-6x)A>N<~8f{ zdpZM4udkWGbMrvO+)Y6%MK*V)o}723X49VX+gqaImR0G!y5W0~Val^9FWJtzhN-*n zEW7*tMD&Sb^_?fxvIC3*_uX4zm3e8MOU@aijVDvpCab8Khiu)nIq(BBx|pj$kU?IMbgM}0)L9{=m!`aQ`RX_8 z)p`Nfz-OryMT{GD)aK+pf2t^!dF(~uN=-A3hUbhTu51@Q-&kgD(ny-q>3ZyP-?6sN ziO&sAvo6%`%CLHSVpZ``#x0&9ik~m>>`?BzHhHC+Vls1*T2t)Rxi?bg+*l&jf4p<% zjo0S|SRyLrgF|&J4oY$htPatd$;@yxcPqP8!CfJlxwwOjACyKAxv`{?DJ>id!<> zYxbsc?lLwue$1Fu6X3I=ye7cs*wzTOw8$GOF~;HQzU$OQ4Y+xe?x;;OZTNV1$LqN! zx8?NCpIj9dwpCHA*|6)eo~KX2l~NJci7aAT-}8#z_503|xL&>ai{GUMp9Q#u+iJ6g zH~Xx~Sj0U2@Hzid;Q+^;)w3ekH*RDyyj&QjC2TCJp0-;yQbAiv*!oR`N~!mym6Mlk znZ1#F;|Z>llV{9$Y;k;*M$-Gz=mVP=#O8lrY-BKZXOn|ZYEopAq-=W5j2X`>{nH|4 zgVL`&H~qGm?bMq52O7KAr6##o>-U}hs(w~kiCIR&_uT%CjE2`s?b9x;oAPY#mhNMl z47rn|o4V_4(xp%LN?n?p6&wUy=6V+@9Qp9|FKdGa zkD`*phV`qiSoL>SiI_gQx?}kw#vM|^W|{M%yOvxRWtA1%aiwYLy4QzFZ}(Z;|1Q|7 z6gDe;hQ*e;>>VLmlA3%EXUNn9cP{zkCii`kW!T&;$%~TRBF&aPyQ!~seR8LtyPIRr zqB>u{q%N&x@*JCYtbi5o^r#vv)kFmHb=xIw>{vsdF-O(w;r-D?5D@(iWYa zlA0)Xr0d*~O&Z2an`W#ud^VFYnK|ic(ajl&b5>7gNs4@1^y>3s1|#XDi8g9?ENn9s zriK0yyZ^y+?wQ5YW^h)|C=@W3{phEVCVBaZ;Res$&zCuMavUjPb=!QWp+&%L)h?#C zUw)IUG#zz}j2CYH;k7<5}Dfj!p2VrTx`lE|1;3^ zKl0+ocbSK!am7NRw!vA#>=}wmk5{Gn%WiBsSb4?ncI7mN(&en_~kGjsCVH&HoT zyQEjCvp(6JkhE)KNo@GOe4`}!13wxiZXVgB=YRIUx?0+%TP>mL-K=q|!i$2;X6Yz@ z3!N=-ey)jS|8c$}pBDUytW@ugs5<8LslsQGmF20Jm(S-uamY0Cy)dOL%_7NOx@uL` zt0skp#});5%ibE?|E{VnJgZ_+;#GH3Io!(_KG3RNflVz+A+g;!4yE39g`D$xdsjSKC{Z+!c^~zpL)ESJaf_p_?{^uUxv) zvLiLL)6#RYl=W;-_6;iA(>YgN_lT0Vg2gM>B9W!5jytNE9yxTx>e17}4B45h*4y{p zJ1K9_`s!6m-s_Kxnfvv^&UhKBtZY20(i+jTDp7@X_pG!em1Op$?(Waxo9-Bvn)`;I z$qiw66k@F}Abq9UyaJDCry7PDWY+uV#}UKM@l3)Z6<{8?b{Nw zBd~sdhnc$E_VS2jwrAd}{GG2JvGQ|3sCs+g%AYciUTbhP$(0)M9KMl$rK+rbuUhb` zC4~Zu4Y)7wzErqcec9`CHZOV3c$pPz`OY<&T9#tucE+M(s@KvpY76*wY+A(|cO=MW z@~hU(3zmrsb3Sk}@yV7`JFH#AxN!};vv`uDs>`M@#;zn`@7?n}qb}HdtiR-#b7kkr z=xv3im#(zgYRV`O(UpYj zD#slH#ZN9_oPDm^I#nP>cHM1B%vM@?Jmm4DPxDr26+B&N zmwL0~MvK>uJK4%xoJrr&%+0B+ug)w$9SBYTcf6_2l6*&cQn; zA33t(^p2IkO}E`Usq^*07A>)T@1->tCp)g0@xU~*FiGud&8E&k&YWOLGc!LO#t4sC z->jQvUB4?Q*(5wJn``Em^X1+umY%HBX2GAc*EpVe^2E1WSo$h!ZWa3$qeDx!`sA#c zA;EAZ>lx4Q{I)!0g$TM4Q$>`ZLUhj&oY?P?a z4f+0C#jQpBZV02gWGF2CE8=+PT$`P{rd(C{a4SmWua^dz54I=zI7U0Y`gY3s#}=GX+C1Qc6YufqwzdGmol9yUygDO zfspKXrN2W;`eoyb7xx!Rw9S}1SJq$P^r^aqY>l7(%N}oh|2s;nW=7{a2aC z8t+NM9UmJCgDZE+iIcA0@d3yjM;4_c`;uc1zS+x!pGnUZn({eh{H>wg05b z%Aa@oN+kp?PqkK`ZS+2P;ie61KDD2Ix@n`DVlr#et;yvwAzGp)QoOmJUTN%>y~)$Z z<)U`B!uHG#pQt%A9$RcY@NvyEm!&}y7xJ^Sv)dkZ-F!>O=bF%I)+3uZDncd2*iWon zsrlyp`^6VCzO0a1EcI;XL*w_YSw}Yo+nwL(n;BBDaBIlgFhg5gUWOa@@4Iil`Q~EP zCW+S@**C7?`R!?4v^DB#(~U&6ny{$`Ya&io@fP-~tzN{nG>G$RmS~{J*9_yxrWq%> z#CA4B?3^>#>SXn!i+M)o=Iv>#TVG2{f3R8qI90GfCO;uexJ6Nk;ZW&?I<_BTwu z6*fP*_~On}SHiLzHMkU9w%3JRd$lZ!UsFi>$zws+i6Lv-UhcJcVf*0C+~en;rnXpP?*`->ezB=+10+zH}-W;j()fCcYD5m?b?&~W8&l69n?RVay;Z>UntM;@Wu5U zqp13v84*(2vuDn16kxi(KEB_#?9(=$MSN-; z)~&g=?xe)V7zZ8i!~66uXNzUT}q(ifIx>{ zJFi*4{(H`hNGpM3Qb#sjYfm<@<^i3){qtufXekIgJA2V_wO=-8e4lmCoWtgR*XH_X zo1F=z3A0T*K8mbUy2<===b5u-7lvr@mi8ZyY*ILI>J-{`l7H>r2kv->S}A-&qnf zIq+C>-1gVIR)lDEsy1z6keatTG9_SvtF^s-{*{O|-(obFI&}5*lM@piS6w)KE_c`d zzQ+r$W<7j!b8|#gRFeP`=&aybXP1QpPLBA0=jg4=8-_(`7c<%H!YOY^S6ba0qH}BY=KYMsM4KkUv znL1h%J3!vCtCi|?Q$99}eP`XgB$xZuUsuQL{;z+x^SRJpAf# z0T$4*9x-w8k5|{%iSFLLyFJhO=-Ytq?rwLh#-F8nIyy)8OkI`}woB%U5~I}iw|B~J z=eoU)?>+Rym+{Bk@_U-5yz@5LzKCIJNZb6j6tsYfpW(*Mn+Lu1_o~?bzVmqYRQIws zjyl}@?w1xRE!(Cj_iNjXKi3^uKxV{0v1OO#oJO=ea)Ab`#&~a zU&-5~aNzTK`~K+dd7cGN?)Gn*6~FAP(J{w0QEQi7W0#-8cl_$!AIe+}*YA~@Hz^$0 z|L63659uI@ZLd{U>YNi~>J#WVB%pMxN~vXGf9p}pGw05gHT}PHq}Y6=*QMh7-vejQ zI&|la4%2=2D;u*p9x8RQtydQfKkEAYnspHuhvJ2dEw>EI=58!(Vc>eOF!=RJ#sk{% zKfQeX{g1z?$h!At0fYNCGgCRkOyM)qH(EL#{I;7mJ-qO^?0Togf=j9bs^2a@<@N0u z1A{Cl=#BtJucg~MpFViN@V?@>wbk|4kM93_yIP>HeVY8Ck9W%EEKz1~S8!o@$mh>= z<+{(bW#@W3U*<$cu5xx1c<|=tW{cWiCj0(_Fb{Y^wS?~_}7N5Ue2_pYH}vi=Lx-wZZ?O7 zhcD%xvrdsCYUav-vuT?rJUDuOdU|%q>d-so_iI1Y+Ly)iA3pYDW2MidK#Q`HN0q6O zKR%zcet%?V@pAzd#wLXhM*Xv?a#a`Qn%6}>iG5!c?ykye_xtAgWnxwqXRNNSUB7XA z=TxtyYZKoJwr`y|$EuVIw9s;1#UoCezh5pJ6^5-%TX}0rM!Eh}FN@MwAv$N5#4`QL zXg&4&-1+mTHFD2?wtDyN@aFx(XX)0B{foBkJ^H;?w_B5G!pbaR$yMQD zVISJe_c-25j=kLNc-({cq~`R~t%-~Gz4y*N^t~38p({>D-)pMcD>s{4=7NT)Hjgvt z!iU5}M*EM#_KfF07d$;>YV+;K;|76(yO%B$rwiQHX1}trUpP7{s>&o+DRt%not6V{ z-stRmUcHYUbV~pHe=p~sdQsga`}pYnj89B&H9iQd`<0YceK85LxaBINz~Kb)L1H4~ z+A!{Y?~gN8x3e0_WxM#evHo0i^YDwfqUh(*_kV>+8oSMX(C#}o(EjpDmnaP}sWwG{ zt)WNv?zP?XHaGupc6@bz&-=yM&+jj+3|f8lL;U~O|9dy5pHIquE%xu<<@v|{)yeif zK58Y?`)cCVrE3Du?%&8}xKDe=y{m@}TozwEurhf02WR^)&O55UYW;iS|BvPN?b}BC zR)lCBJ?GBRc=V{Nj=uil7`@Xs7C*Y#ax3e}OyhI|W8=%g!nFa`t-q{%zbH+wxi;5) zCWGG`i$-?&8iC;8;7wC{R%!8{k~?JJvNOhQZP@8O1&(6Z{nyNV_I~%huX;77x8LDD zUKp_cdNSw7FaIAsQu=qh{lGas_C8>j~xl2cZ1$^73-kt*W!4S6?+S zw(c(4*;BQ*FX-mfc`J3!7hd_u+>o}pZ|hd!9b5PIYKN~=;Aom?W1lGDb+{thx7WPu z#I7r6SNI$$ZVj1V5jhjIQvJN$Z=H3qyT!W8rH^em@7N=~$hYjM-5HtXJ2&j#-|w}w zseIq(xfbQ`c;?mp%FM~jTXZ#RVbDrYAq3tU@qOR-y))*`J1V~KgX^Ey`~PW|eg77) z(#P}LvTr%IJ!_+6)AvoB@vdZwbjMULRT&wXE#(t5qD_tL3MQJ=usb$nZS4X%0<@Od zM9OI8Y__R3GZ-FhkNOZJ&cWL9Sb<{&d%U5VLAu}e{f|UBni;0+#X6n*%#b$c>euH> zvP0tEuR1fYXL<1Mmm-G_-0ytLEW_dWJxYOxQBj6LaNqym`{hBa;(PAiz1!G0SASdB z>ei)8RnMF~yL8iaWyX&j1+KFzm5Qty;@nnCD)1=ZDSd8hQU8t)bja$12Mp_8gEl=F z8yi==nZLgy`}*~Z872a*f~THl`A_=4!t}`{t@$CVL#Ld6THd$U$}=|f(+q(uW0p^E z0-17m#>^VNv-ar25kIl4c)2gnVC|vQ}U;65qGd$C#O#|&ONZZ`` z`uh6EPp8M5h3(6n@cU7v+_Y)amk=PX^S`Ui9@`n@m9%oEGX#D4$&y^&R} zZ6VK_xwB^<-dX&-;MEn)oj0xonLK7ldQ$s*w%B^hWvltqU+r6c{4Dd4J$u-8&VM(j zmuscDUe@brn~nBARyeY);*9j$b?Sa|4*dCJ!^g*$vVyVpcxkNUj+2j9^i5vzZLZhS zCeSJEhqL28OM&)MBquYUO%ryo?$1w5WCR_&J9lpH&i|Sv&U3?E(_inplftF>sW$#~ zz-e#cQ_3N#;R2_tpFf`*pu4wvimj@9?1~awkrm#_cX+SxZsiTwsZ~0sp~wAmaoOG- zJ34;;tlaZy>iR3sPJjBn{@JNZGLquO%~8U7W@fW$+$MjYpKX8KZEi@Y*<5S0)3FZQ z1AZR()S0c zdR^w^vuh^UaVhw}tEuvvx9Cc~rv1FWV{iKxGkp8RWb|ooyY1<+HvYi-w?fS}zD=`B zoa-AVxZ`4Vgwm($rw{Fj?cbAMv~CX16`Rbcc{Xi(+itwi)ec%&5xs)v+p4Mew^vWd zXHQrWbnH;jxowyEj(z7p^vp(}^irp*-~wNNw!2fs~u?nmU+6)td#(*QcdLZFyto=Jq(+*ibiWOOHsyNxsFghmW+aJTOO0 zH)@;v(vYQQZ?#G{UGH*_+LCcoHcLhR{%)-src%8&!Yqt`%n}kzCHG90xOZ)@L-(|a zY=(=qFU}Faa_60^6B}F7vdnX1a%~~d=8&x-7OH==xlj-X+USaR$m+e39^0S#3 z**rr`cd_m#u2(A4Qh8P0o{)LcTy*7Gk59Ll?&39m>t?lAxZd5kabx3@T~ZPq7Z0Bh zmE6)8-#zUjyW!3=qF04(o4!(EnCNU;(jAa-aZ#$b2bbvEC0iAAP9NG=`+HmLT`7qU zRiRG3CCko7ZOM?#ja(ab(KjGyupzdi8g)13FD`kVDwjBFxGh-R&@xl+&_upx2X6NTi zap0fw?9bc$a>;eCl_q+u(Mb{9cjT6a?v(|ur{!BaPv-Pob6A@C$m-x)JHL50mKGCa zOPbG|Idh;i_QtlgF*9A&+p{kk%~5c>CA_`<;;QJjLZ6k;L57T z<{QlXchUn~)@QEMjn%z<)91QQ?QzyVZQa)#p~H6m^M*Y!M?pJGo!j|% zr&PY-=#c$=IZ%13*THSMJJ#K1O`4Y4sNVg4TdCmI3`x_EHvSdzx~H3RZ+EyY?r>YI zq^BcfIN|iuf?r==Hm>%w7Tf>f#L4!a`*nX=`*IJK#%}zxCpEI;Xp)4qbaU?QxvT+u z&3q&bm=rR88!!pGrANIuvStQj!jDsuDn6=jld836%d?!?pIEjl?)v>RIpS&E0cSh* zo{Q%1j(z{?{@-;QZ>HZlwe5ChpswE?LHRbvw23EXedDRwf4N=sZLWmda(`(%CgyCj z8Erq_D8Kl{efh;NxjF`Ah8_lk#E!EsNWJ5BzCrOA9@@=RL%DIwA}{cTajpM4d7 zWbOOYeOH{jn^Bj&|J#K1=2{GD+vl#@f8uZ2+uc0Z)>!yoSA4wV&bQUGIs~iiQ#LQ! zHue72Ly_YDcvx@0KXt!hJi~@4KrAIj#&jcIS@FxA*t+ z|2za|i}m~eS^fL?ClZvrw&_mX$KbF;KftXq6Xd6GUcObtI5wsLt};(={G z5^M=lj6TW@jPV<6SQry#obuZx+Z@0urFG!Rd3F26d-UJfgi3e|9O5yvdJ(!`a9XdS zSB?(Pm8aXD|IPine*dMV*VxKdi`^1f@ceg!pn?I@j}PI$(xo|SBsd=Ilx_HQJNL)i zZ45ebOC}tWSDNS%-eB3kVfpW@^)ZXDW-Yv#v*=>hp-nnhUYD9kou1oeqWxS~SNGv* z{ryKSE_N?}H}_3S`;S+v*Eb2P`zcJm3)jrIJfO?)*`McyBM=d=kF;LyKK&hM4O|_4og2^0)sha`chXx%u|}?((%)ejIL;V3=c5Y4qd8;(mc%x5E3i{13jo zyu9OKoAj_dF9e32>U}6k1(zsacw)mo{ z!%<1m^ia^k7fyjJv)N2bVy=raxH&xCX(!d&79h!a=EaX`*IjnsbyE>?+@UE=`0wlb|Ht?L|7$B(`9$!~ z-}m23CmoCQ;d=S%PG7?9ZHM|El%91-TqPSkIR>(?&;qM3;$u>NVCB?fCjxCQtRi${XV5 ze^$*1JAP6$Pdu1a=(8`|udhrIGpu&H`6;#L_gV8DMNh*` zX8X=q#(8=Z=q8-xva&Mw!W0dMoVq%@9na@ggA~rto+mQvAfK#Ni?I5;BOf0hFZ}W% z(8tH88+alESM z@qhlDIY%}oA1}C_yZzybiOM@Z9+NgSG~77JC$D6w{zuWUkZ%TY@8?%%J=x_^^B`wi z@b}MGa;L76Dx15Z=)tPm#H`dA=6S~7ul2udtG&Pe?Qh%rZnrJYtlk|dz{8!$x!L>F z?+(q2|Br0y=oDr+xk|#WUw!}kE7NU-nc?dztR-YcbYHei|;bKj^wQ%45U(s_n{=AjF z9yFxud~sphy72w~|NTA)8iXv`sUsn}viFJN!cX#ZH69tN2yyoG_bX5JQk;BJVftys z>8BIl-`jg4#puTN{P`AY6Fn5<`nv^sJyb3|HF&`uFq={CRmWep_E%MV*^(qQzU}IJ zweQfCBY}ed&Q-B0=Zh@4W*ynOb#BeStcT54Ul&%V>zf^O?3vBDqAh&$O_AH5J{f6E zo8wCbvsn3!0Aj@ZQW&jU3?0yIPdz-K}Rt_^!wDVMc%mcfz6 zuPc3iZCQrZ$$)RyK%>(Z)!%gLemrDnnCm6JhdE$zkb=(lxJl-!QoTxI-3JS0jyx^| z`D(5o_pxbb(_%O`znj6EPk(09Qo?d9tjrDu) zz0NNWcvB^Dx9Z-^GaD>^e5j4#=2$&f{k#74(!llC1J_??^$_iL4bc(>-6Ap7%atKy zVZeb)mxR2Q3T1)L!D#FF`zT9u{*IkuPNCDEMr=tj>NLMsk$n8oK{u`=3=dviU42k{ z{hp@sdzJ1hic$i$U-RwQGfIH3EpcpW=p20-8Rd)E#|rQ$Tf}&%a$!;**$yC z92dRmhp$`_Id=TGVE=K~I`x}(f0b5=DnHgK*nKcBOyRlAxoCbfgQEpTPHN;?0NpC@3zJZ)ydj3*)JbUGCcTZ(n*z|l_JNE9WyXARNNM# z)q3)zr-~3~z)`-WwkbFCtVQeeALL9C{5S9MHt7`eMOOJ+-kNdO?9J)5`Qo_b^_m9Z zn+rU8SBI^3x~mL2nYt-5@CEbD{Tf~Kb9QTOomoBM@FAlgM{d> zH0S|3lHu8znG270O*E|A^Yhv4gVFhWL3`K_70N8My0l1HB4TZHm^MPXwg?$+8=te)U9G zI6=;?M#A6jr%GJS$D<36cT8N}T=ehH&j)+I-#dI zD9z9~RVMz}{^{KoMGRA{HT_Ra?Y(%X`qcZgCcZtry``cHl+D~%y?OFPMdrBR@8pd$ z80`5PrllS%lwn*m<*h*X(Q8lcp8US~vX4)n%5%rLy>8AcLW&|78HLVsE)kJtT2-`@ z=X&Yx9QQA>Hy4Gt37`rC!3~WR9lJp?UJk**r26LLMBGoPFjN&tZqnH(hMx zj)M-f0Zpm=`~CjE)%4SUOGOM4Q@Sl<)m^=3<=zN>A=JyrmpI>}4r>oKPfP%5c z)nmdtEIC#-1TNk6>9a-g;{z+K^mO=x*BM<{pT#C`a70Ua;t8pKm6Z=zA}(f#Ft{+; z-emK9VQkB6>7BAId6rtz#u*81s{}+vm6utl+I4VV;moLz4#<;dI4Dtb|Gk&rGs7U6 zl9b&l8`Gt(XtnviZ#?d~@{`)(5K}%C8JiFv4>mEY_S#-Ft7Pc3h*4^GCqxNPN(>HOw7><4a|5byo+^MUR`&k1*N0S84 zrUkFmD+-v98)Ed}M41WKI@Jdc8ZL3OgwFq2xk{(0PT@@JZJtU~zt9(rF~>UUx~=R> zE9%y6xZ^jABcx|ZIIo^1U)TNSImWBGc1@YNR_jfpe%e9Dp7_bvPfZeDb@I~=9`C2E1qTPum^bg(+wJ%J7Q6TV zyMN`%6?^x}H}8LiT73HXXLb1cxWD(E7B>9+St)3|dB={9*VorC4$cUyN4GthqIk1SeZKp7 z7tgyPv%K=;Pj50XGgHf1`NZ~o+GbEi`n2fA_I!DxnLOX#-QAtc%5B=g8}RJ;^X8XT9EHWMZh$tydeq{7bo zgc(2m<23b^JG1A}3x{%g65Hw}6GB#PIJIKkrqd^U^mKT=(p2tzTf!tb)37jLs-rib zy~4F+OdfnmMj2nV9xLb9JdD&j>v!+n$B%_??(P;}8N#(QhHs{iTgCp%FHQJlEIzc$ zAC@{|rZ`Zn`UTe*a#|ECwunv_uEf6nUeO^_S(oK16&ug-(_gtu3-i&FHvfG)9w)UKx+-x42)3e&% z=Ee->Ia*J<{_9`w;_i-{?#%MS;@90e)m=NeG;g2y@jvmN^FABN`zIdHKAXmB;Opnt z^~LOymipwA6SrGu3S8c#xwGtT)WOobCR2SCL$`&k?mWL`zS1`7-Zl{u8^8 zuYNWuxoJy$3^mgF4}~gC^tia~?fzw#U(PQ%e??(gDtqL@_rJgIUiaHxgxU2W z=hv!OhPcf)MH((oO0DsBc=J<#~)^0wTl&!xFi+J zM0(x!Z_{}_KT7rTBG+y)-KZ@Y7cm%;MnC!Wvzzx?n{Z8IU|?YIboFyt=akR{0IVE{ An*aa+ literal 0 HcmV?d00001 diff --git a/client/images/gopher.svg b/client/images/gopher.svg new file mode 100644 index 0000000..171ecec --- /dev/null +++ b/client/images/gopher.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/logo.svg b/client/images/logo.svg new file mode 100755 index 0000000..de3f45d --- /dev/null +++ b/client/images/logo.svg @@ -0,0 +1,20 @@ + + + Layer 1 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..f81c052 --- /dev/null +++ b/client/index.html @@ -0,0 +1,52 @@ + + + + + + + bab + + + + + + + + + + + +
+
+

Bab

+
+
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..ad059c5 --- /dev/null +++ b/client/package.json @@ -0,0 +1,11 @@ +{ + "devDependencies": { + "grunt": "^0.4.4", + "grunt-bower-task": "^0.3.4", + "grunt-contrib-connect": "^0.7.1", + "grunt-contrib-coffee": "^0.10.1", + "grunt-contrib-watch": "^0.6.1", + "grunt-concurrent": "^0.5.0", + "grunt-contrib-clean": "^0.5.0" + } +} diff --git a/client/partials/about.html b/client/partials/about.html new file mode 100644 index 0000000..aadd426 --- /dev/null +++ b/client/partials/about.html @@ -0,0 +1,25 @@ +
+
+

+ Thanks for using Bab. +

+

+ The source code of the application is available on + bitbucket. +

+
+
+ +
+
+ +
+ +
+ +
+
+

This program has been done with Go!

+
+ +
\ No newline at end of file diff --git a/client/partials/home.html b/client/partials/home.html new file mode 100644 index 0000000..91cc64d --- /dev/null +++ b/client/partials/home.html @@ -0,0 +1,10 @@ +
+
+
    +
  • + +
  • +
+
+
+ diff --git a/client/partials/login.html b/client/partials/login.html new file mode 100644 index 0000000..64aa0bd --- /dev/null +++ b/client/partials/login.html @@ -0,0 +1,27 @@ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..8879685 --- /dev/null +++ b/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "io/ioutil" + "launchpad.net/goyaml" + "log" + "net/http" + "path" +) + +const helloworld string = ` + ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ +||s ||||e ||||r ||||v ||||i ||||n ||||g ||||: ||||8 ||||0 ||||0 ||||0 || +||__||||__||||__||||__||||__||||__||||__||||__||||__||||__||||__||||__|| +|/__\||/__\||/__\||/__\||/__\||/__\||/__\||/__\||/__\||/__\||/__\||/__\| + +` + +type WebAppServers []*WebAppServer + +func (this WebAppServers) Handle(w http.ResponseWriter, request *http.Request) { + for _, was := range this { + if was.Subdomain == mux.Vars(request)["subdomain"] { + was.Handle(w, request) + return + } + } + fmt.Fprint(w, "could not find a web application for ") +} + +func (this WebAppServers) Localize(domain string) WebAppServers { + fmt.Println(">>", domain) + ret := make(WebAppServers, len(this)) + for i, was := range this { + ret[i] = was.Localize(domain) + } + return ret +} + +var webAppServers WebAppServers +var port int = 8000 + +func init() { + var err error + webAppServers, err = loadWebAppServers() + if err != nil { + panic(err) + } +} + +func loadWebAppServers() (WebAppServers, error) { + data, err := ioutil.ReadFile("webapps.yml") + if err != nil { + return nil, err + } + return loadWebAppServersFromYamlFile(data) + +} + +func loadWebAppServersFromYamlFile(yamlFile []byte) (WebAppServers, error) { + ret := make(WebAppServers, 0, 10) + err := goyaml.Unmarshal(yamlFile, &ret) + return ret, err + +} + +func serveStatic(router *mux.Router) { + handler := func(w http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + filepath := "/" + vars["path"] + w.Header().Set("Cache-Control", "public, max-age=43200") + http.ServeFile(w, request, path.Join("client/", filepath)) + } + router.HandleFunc("/{path:.*}", handler) +} + +func ServeWebApps(router *mux.Router) { + router.HandleFunc("/{path:.*}", webAppServers.Handle) +} + +func WebappHandler(w http.ResponseWriter, request *http.Request) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + encoder.Encode(webAppServers.Localize(request.Host)) +} + +func createMuxRouter() http.Handler { + r := mux.NewRouter() + ServeWebApps(r.Host(`{subdomain:\w+}.{domain:\w+}.{topleveldomain:[a-z]+}`).Subrouter()) + ServeWebApps(r.Host(`{subdomain:\w+}.localhost`).Subrouter()) + ServeWebApps(r.Host(`{subdomain:\w+}.\d{1-3}.\d{1-3}.\d{1-3}.\d{1-3}`).Subrouter()) + r.HandleFunc("/webapps", WebappHandler) + serveStatic(r.PathPrefix("/").Subrouter()) + return r +} + +func main() { + fmt.Print(helloworld) + address := fmt.Sprintf("0.0.0.0:%v", port) + if err := http.ListenAndServe(address, createMuxRouter()); err != nil { + log.Fatal(err) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..f913cba --- /dev/null +++ b/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" +) + +func TestLoadWebAppServersFromYamlFile(t *testing.T) { + yml := []byte(` +--- + +- + name: mongs + url: http://localhost:3333 + subdomain: mongs + image: images/mongo.png + +- + name: ipython + url: http://localhost:8888 + subdomain: ipython + image: images/ipython.png +`) + if was, err := loadWebAppServersFromYamlFile(yml); err != nil { + t.Error(err) + } else if len(was) != 2 { + t.Fail() + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3e25ab5 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Bab + +Bab is a gateway to your web applications. Run \ No newline at end of file diff --git a/webapps.yml b/webapps.yml new file mode 100644 index 0000000..d6e6851 --- /dev/null +++ b/webapps.yml @@ -0,0 +1,15 @@ +--- + +# This yaml file where your need to define your services. Look at the *http bin* +# example for fields one needs to give to define a service +# +# Before wishing to use *bab*, one needs to setup correctly its DNS server to points +# the subdomains to the correct ips. For localhost case, we recommend changing the +# /etc/hosts file to have a file similar to the one bellow +# 127.0.0.1 localhost httpbin.localhost + +- + name: http bin # service's name as it will be shown to the user + url: http://httpbin.org # service's url + subdomain: httpbin # subdomain where the service will be reachable + image: images/bin.png # service's imagae path relative to the client directory diff --git a/webappserver.go b/webappserver.go new file mode 100644 index 0000000..751e9f0 --- /dev/null +++ b/webappserver.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" +) + +var _ = fmt.Print + +type WebAppServer struct { + Subdomain string `yaml:"subdomain"` + WebAppURL string `yaml:"url"` + ImageUrl string `yaml:"image"` + Name string `yaml:"name"` + Domain string `yaml:"default_domain,omitempty"` +} + +func (this *WebAppServer) Localize(domain string) *WebAppServer { + return &WebAppServer{this.Subdomain, this.WebAppURL, this.ImageUrl, this.Name, domain} +} + +type jsonWebAppServer struct { + Name string `json:"name"` + Url string `json:"url"` + Image string `json:"image"` +} + +func (this *WebAppServer) copyHeader(from, to http.Header) { + for key, val := range from { + to.Set(key, strings.Join(val, ";")) + } +} + +func (this *WebAppServer) GetWebAppBaseUrl() string { + return fmt.Sprintf("http://%s.%s", this.Subdomain, this.Domain) +} + +func (this *WebAppServer) MarshalJSON() ([]byte, error) { + ret := &jsonWebAppServer{this.Name, + this.GetWebAppBaseUrl(), + this.ImageUrl} + return json.Marshal(ret) +} + +func (this *WebAppServer) HandleGet(w http.ResponseWriter, request *http.Request) { + URL := this.WebAppURL + request.URL.Path + if request.URL.RawQuery != "" { + URL += "?" + request.URL.RawQuery + } + req, err := http.NewRequest(request.Method, URL, request.Body) + if err != nil { + w.WriteHeader(500) + fmt.Fprint(w, "bab error :(", err.Error()) + return + } + this.copyHeader(request.Header, req.Header) + resp, err := new(http.Client).Do(req) + if err != nil { + w.WriteHeader(500) + fmt.Fprint(w, "bab error :(\n", err.Error()) + return + } + this.copyHeader(resp.Header, w.Header()) + w.WriteHeader(resp.StatusCode) + defer resp.Body.Close() + io.Copy(w, resp.Body) +} + +func (this *WebAppServer) Handle(w http.ResponseWriter, request *http.Request) { + switch request.Method { + case "GET": + this.HandleGet(w, request) + case "POST": + this.HandleGet(w, request) + case "PUT": + this.HandleGet(w, request) + default: + w.WriteHeader(500) + fmt.Fprint(w, "method "+request.Method+" is not yet available") + } + +} + +func (this *WebAppServer) CanHandle(host string) bool { + r := regexp.MustCompile(`[:\.]`) + domain := r.Split(host, -1) + if len(domain) < 3 { + return false + } + return domain[len(domain)-3] == this.Subdomain +} diff --git a/webappserver_test.go b/webappserver_test.go new file mode 100644 index 0000000..e6fa73d --- /dev/null +++ b/webappserver_test.go @@ -0,0 +1,213 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type TestServer struct { + Server *httptest.Server + Get func(w http.ResponseWriter, request *http.Request) + Post func(w http.ResponseWriter, request *http.Request) + T *testing.T +} + +func (this *TestServer) ServeHTTP(w http.ResponseWriter, request *http.Request) { + if request.Method == "GET" { + this.Get(w, request) + } else if request.Method == "POST" { + this.Post(w, request) + } else { + this.T.Error("cannot execute ", request.Method, " method") + } +} + +func (this *TestServer) HelloWorld(w http.ResponseWriter, request *http.Request) { + fmt.Fprint(w, "hello world\n") +} + +func (this *TestServer) ReturnPath(w http.ResponseWriter, request *http.Request) { + fmt.Fprint(w, request.URL.Path) +} + +func (this *TestServer) ReturnBogusHeader(w http.ResponseWriter, request *http.Request) { + fmt.Fprint(w, request.Header.Get("Bogus")) +} + +func (this *TestServer) ReturnBody(w http.ResponseWriter, request *http.Request) { + io.Copy(w, request.Body) + +} + +func (this *TestServer) SetBogusHeader(w http.ResponseWriter, request *http.Request) { + w.Header().Set("Bogus", "has been set on the server") + fmt.Fprint(w, request.Header.Get("Bogus")) +} + +func (this *TestServer) ReturnParamValue(w http.ResponseWriter, request *http.Request) { + if err := request.ParseForm(); err != nil { + this.T.Error(err) + return + } + fmt.Fprint(w, request.Form.Get("search")) +} + +func GetTestServer(t *testing.T) *TestServer { + ret := new(TestServer) + ret.T = t + server := httptest.NewServer(ret) + ret.Server = server + return ret +} + +func TestMarshalJSON(t *testing.T) { + wapp := &WebAppServer{"big", "http://bigtits.com", "img.png", "some name", "tits.com:8000"} + b, err := json.Marshal(wapp) + if err != nil { + t.Error(err) + } + if s := string(b); !strings.Contains(s, `"http://big.tits.com:8000"`) { + t.Fail() + } +} + +func TestCanHandle(t *testing.T) { + was := &WebAppServer{Subdomain: "foo"} + exps := map[string]bool{"foo.bigtits.com": true, + "foo.localhost:8000": true, + "bar.bigtits.com": false, + "bigtits.com": false} + for host, exp := range exps { + if was.CanHandle(host) != exp { + t.Error("error handeling ", host) + } + } +} + +func TestHandleGetHelloWorld(t *testing.T) { + ts := GetTestServer(t) + ts.Get = ts.HelloWorld + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if body := string(w.Body.Bytes()); body != "hello world\n" { + t.Error(body) + } +} + +func TestHandleGetPath(t *testing.T) { + ts := GetTestServer(t) + ts.Get = ts.ReturnPath + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if body := string(w.Body.Bytes()); body != "/natasha" { + t.Error(body) + } +} + +func TestHandleGetBogusHeader(t *testing.T) { + ts := GetTestServer(t) + ts.Get = ts.ReturnBogusHeader + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha", nil) + if err != nil { + t.Error(err) + } + req.Header.Set("Bogus", "something in the header!") + was.HandleGet(w, req) + if body := string(w.Body.Bytes()); body != "something in the header!" { + t.Error(body) + } +} + +func TestHandleGetSetBogusHeader(t *testing.T) { + ts := GetTestServer(t) + ts.Get = ts.SetBogusHeader + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if header := w.Header().Get("Bogus"); header != "has been set on the server" { + t.Error(header) + } +} + +func TestHandleGetReturnParamValue(t *testing.T) { + ts := GetTestServer(t) + ts.Get = ts.ReturnParamValue + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha?search=bigtits", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if body := string(w.Body.Bytes()); body != "bigtits" { + t.Error(body) + } +} + +func TestHandlePostWithBody(t *testing.T) { + ts := GetTestServer(t) + ts.Post = ts.ReturnBody + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "http://foo.bigtits.com/natasha", bytes.NewBuffer([]byte(`hello world in the body`))) + if err != nil { + t.Error(err) + } + was.Handle(w, req) + if body := string(w.Body.Bytes()); body != "hello world in the body" { + t.Error("wrong body :", body) + } +} + +func TestHandlePostBogusHeader(t *testing.T) { + ts := GetTestServer(t) + ts.Post = ts.SetBogusHeader + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + w := httptest.NewRecorder() + req, err := http.NewRequest("POST", "http://foo.bigtits.com/natasha", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if header := w.Header().Get("Bogus"); header != "has been set on the server" { + t.Error(header) + } +} + +func TestCanHandleNoService(t *testing.T) { + ts := GetTestServer(t) + + was := &WebAppServer{Subdomain: "foo", WebAppURL: ts.Server.URL} + ts.Server.Close() + w := httptest.NewRecorder() + req, err := http.NewRequest("GET", "http://foo.bigtits.com/natasha?search=bigtits", nil) + if err != nil { + t.Error(err) + } + was.HandleGet(w, req) + if w.Code != 500 { + t.Error("status should be 500, service not found") + } +}